I'm attempting to make a user-defined function that mimics my formula. I need something more efficient than my formula.
I've tried VBA and using the above formula as is. This is ineffective for the larger datasets I'm working with.
=IF((AND(B2>=65,A2>=7)),"Greenbox",IF((AND(B2>10,A2=0,B2= "")),"Balance",IF((AND(B2>=65,A2<3)),"Yellowbox",IF((AND(B2<65,A2>=7)),"Purplebox",IF((AND(B2<65,A2<=3,A2>=1)),"Orangebox",IF(AND(B2>=65,A2<7,A2>=3),"Bluebox",IF(AND(A2<7,A2>=3,B2<65),"Redbox")))))))
A VBA function that mimics the formula.
If you do not want to supply an input to the UDF, you can grab the row number and worksheet using Application.Caller. Otherwise, you can add two arguments for the A2 & B2 range and compare the value there.
I have made no performance tests regarding the two methods, but I would imagine the one that does not use Application.Caller would be the one that has greater performance - but I figured another example wouldn't hurt.
Application.Caller Method
Function myFunc() As String
Dim r As Long, ws As Worksheet
Set ws = Application.Caller.Worksheet
r = Application.Caller.Row
If ws.Cells(r, "B").Value >= 65 And ws.Cells(r, "A").Value >= 7 Then
myFunc = "Greenbox"
ElseIf ws.Cells(r, "B").Value > 10 And ws.Cells(r, "A").Value = 0 Then
myFunc = "Balance"
'.... Continue
End If
End Function
Which the worksheet formula would look like: =myFunc(). (no arguments needed)
Function with Arguments Method
Function myFunc(rngA As Range, rngB As Range) As String
If rngB.Value >= 65 And rngA.Value >= 7 Then
myFunc = "Greenbox"
ElseIf rngB.Value > 10 And rngA.Value = 0 Then
myFunc = "Balance"
'.... Continue
End If
End Function
Which the worksheet formula would look like: =myFunc($A2, $B2).
As already mentioned in the comments by Scott Craner, AND(B2>10,A2=0,B2= "") isn't logically correct. B2>10 and B2="" will never be True when used together with AND, so you may need to figure your intentions out with that one.
Here's a go. Total is the value returned in the cell with the function called.
It would be cell a2 and B would be cell b2 in the example
=caseTest(A2,B2)
Per Scott's note, <10 is used rather than null. Feel free to edit.
There is also a default value if none of the conditions are met. Happy coding.
Function caseTest(A, B)
Dim scoreA As Integer, scoreB As Integer, result As String
scoreA = A.Value
scoreB = B.Value
If ((scoreA >= 7) And (scoreB >= 65)) Then
Total = "Greenbox"
ElseIf ((scoreA = 0) And (scoreB <10)) Then
Total = "Balance"
ElseIf ((scoreA < 3) And (scoreB >= 65)) Then
Total = "Yellowbox"
ElseIf ((scoreA >= 7) And (scoreB < 65)) Then
Total = "Purplebox"
ElseIf ((scoreA <= 3) And (scoreA >= 1) And (scoreB < 65)) Then
Total = "Orangebox"
ElseIf ((scoreA >= 3) And (scoreA < 7) And (scoreB >= 65))) Then
Total = "Bluebox"
ElseIf ((scoreA >= 3) And (scoreA < 7) And (scoreB < 65))) Then
Total = "Redbox"
Else
Total = "default"
End If
caseTest = Total
End Function
if you need help on creating custom functions below link is an useful guide
https://support.office.com/en-us/article/create-custom-functions-in-excel-2f06c10b-3622-40d6-a1b2-b6748ae8231f.
To answer your question, I have created a custom function based on what I deciphered from your formula.
Function Cust_SetBox(A as Range, B as Range) As String
'function will receive two parameters A and B as ranges and return back a string
Application.Volatile 'this ensures that formula will update when cell values are modified
'Original formula
'=IF((AND(B2>=65,A2>=7)),"Greenbox",IF((AND(B2>10,A2=0,B2= "")),"Balance",IF((AND(B2>=65,A2<3)),"Yellowbox",IF((AND(B2<65,A2>=7)),"Purplebox",IF((AND(B2<65,A2<=3,A2>=1)),"Orangebox",
'IF(AND(B2>=65,A2<7,A2>=3),"Bluebox",IF(AND(A2<7,A2>=3,B2<65),"Redbox")))))))
'adding .value="" condition as emtpy cells will show up as true while checking for X.Value<n
If B.Value = "" And A.Value = "" Then
Cust_SetBox = "Unknown"
ElseIf (B.Value = "" Or B.Value > 10) And A.Value = 0 Then 'you might want to check this condition as it is not clear from your formula
Cust_SetBox = "Balance"
ElseIf B.Value >= 65 And B.Value <> "" Then
If A.Value >= 7 Then
Cust_SetBox = "Greenbox"
ElseIf A.Value < 3 Then
Cust_SetBox = "Yellowbox"
ElseIf A.Value < 7 And A.Value >= 3 Then
Cust_SetBox = "Bluebox"
End If
ElseIf B.Value < 65 And B.Value <> "" Then
If A.Value >= 7 Then
Cust_SetBox = "Purplebox"
ElseIf A.Value <= 3 And A.Value >= 1 Then
Cust_SetBox = "Orangebox"
ElseIf A.Value < 7 And A.Value >= 3 Then
Cust_SetBox = "Redbox"
End If
Else
Cust_SetBox = "Unknown"
End If
End Function
To quickly add this function to your workbook.
use Alt+F11, insert a new module and add above code to the module. You should be able to use this new function as a formula.
Go to any cell and type the below
=Cust_SetBox(A1,B1)
The cell will show the value based on the logic above. Detailed explanation in the link above.
Note
Make sure Calculation is set to automatically calculate (in Formulas menu -> Calculation options) else press F9 to calculate
The workbook has to be saved as a macro enabled file else the function will not be available later.
Related
I'm currently working on a Quality Concern log that will be used at my place of work to track the quality concerns, and output specific data to the management via a dashboard.
One of the calculations i have, go through rows in a log and counts the number of rows that meet a certain criteria. It is essentially a CountIf function, but with a For loop. The count is then dumped into a cell, and the calculation moves onto the next value in the range.
I've currently got 95 entries into the log and the counts are running pretty slowly. As we get more quality concerns, its inevitable that the code will start to run even slower.
This is a sample of the code i'm running:
For Each cell In mnthRng
monthVal = cell.value
YearVal = cell.Offset(-1, 0).value
num = 1
Total_prjctCount = 0
For i = LBound(prjcts) To UBound(prjcts)
PrjctName = prjcts(i)
included_in_calcs = prjctYesNo(num, 1)
If included_in_calcs = "YES" Then
Total_Count = 0
For j = 8 To IDLastRow
If QCRLogSheet.Range("AI" & j) = monthVal _
And QCRLogSheet.Range("AK" & j) = YearVal _
And QCRLogSheet.Range("D" & j) = PrjctName Then
Total_Count = Total_Count + 1
Else
End If
Next j
Total_prjctCount = Total_Count + Total_prjctCount
End If
num = num + 1
Next i
cell.Offset(1, 0).value = Total_prjctCount
Next cell
Just to give you some more information on the code:
mnthRng is a cell range containing different months.
The array prjcts contains the name of the various different projects we have onsite, and allows me to sort the data out by project is someone unticks the "include in calculations" box on the dashboard
I've read that to speed up calculations of this nature, instead of looping per cell, i could add the range to an array, and do the count in the array. Unfortunately i'm not sure how i go about adding my data range into an array and then looping through it.
Any help would be much appreciated!
Untested:
Dim arrMonth, arrYear, arrProj
arrMonth = QCRLogSheet.Range("AI8:AI" & IDLastRow)
arrYear = QCRLogSheet.Range("AK8:AK" & IDLastRow)
arrProj = QCRLogSheet.Range("D8:D" & IDLastRow)
For Each cell In mnthRng
monthVal = cell.Value
YearVal = cell.Offset(-1, 0).Value
num = 1
Total_prjctCount = 0
For i = LBound(prjcts) To UBound(prjcts)
PrjctName = prjcts(i)
included_in_calcs = prjctYesNo(num, 1)
If included_in_calcs = "YES" Then
Total_Count = 0
For j = 1 To UBound(arrMonth, 1)
'nested if's are faster...
If arrMonth(j, 1) = monthVal Then
If arrYear(j, 1) = YearVal Then
If arrProj(j, 1) = PrjctName Then Total_Count = Total_Count + 1
End If
End If
Next j
Total_prjctCount = Total_Count + Total_prjctCount
End If
num = num + 1
Next i
cell.Offset(1, 0).Value = Total_prjctCount
Next cell
I know this will be a stupid simple answer, but it's killing me right now...
For Each cell In rng2
If cell.Offset(0, -13) And cell.Offset(0, -12).Value <> "" Then
cell.Value = 1
Else
cell.Value = 0
End If
Next cell
Where the .offset and .offset is, how can i just say
If cell.offset(0,12:13)
I know that's not it... This one is simple however, getting this piece right will save me so much headache throughout the vba code all over the workbook.
Thanks for helping this rookie in advance!
Maybe something like this:
For Each cell In rng2
If Application.CountA(cell.Offset(0, -13).Resize(1, 2)) = 2 Then
cell.Value = 1
Else
cell.Value = 0
End If
Next cell
Alternative using variant array
You can combine logical assumptions by multiplication (True * True corresponding to -1 * -1 = 1 in case of both reference cells greater"") and try the following:
'a) assign both offsets to variant 2-dim array
Dim v: v = rng2.Offset(0, -13).Resize(, 2)
'b) get values
Dim i As Long
For i = 1 To UBound(v) ' check each row
v(i, 1) = (v(i, 1) > 0) * (v(i, 2) > 0)
Next i
'c) write values
rng2.Resize(, 1) = v
I am attempting to create an Auto-grading test of sorts in Excel.
I have 5 values in Sheet1 that are input by a user in cells E5:E9. These should then be compared against a range of 5 more cells in Sheet2 (also cells E5:E9).
As the user might not always list these entries in the same order that I have in my Sheet2 range, I decided that I should loop through the range for each cell's input.
The next step would be to be able to ignore the value in the range once a match has been found but I need to get this part working correctly. Currently, the values absolutely match. However, I am not getting the correct output.
Sub Q1()
Dim i As Integer
For i = 5 To 9
If (Sheet1.Cells(5, 5) = Sheet2.Cells(i, 5)) And (Sheet1.Cells(6, 5) = Sheet2.Cells(i, 5)) And (Sheet1.Cells(7, 5) = Sheet2.Cells(i, 5)) And (Sheet1.Cells(8, 5) = Sheet2.Cells(i, 5)) And (Sheet1.Cells(9, 5) = Sheet2.Cells(i, 5)) Then
Sheet1.Cells(5, 6) = 1
Exit For
Else
Sheet1.Cells(5, 6) = 0
End If
Next
End Sub
I would expect the output of 1 to Sheet1 cell E6 but I am currently getting 0. Thanks!
Little Complex :)
Sub Q1()
Dim i As Integer
Dim j As Integer
Dim chck(5 To 9) As Boolean
For i = 5 To 9
For j = 5 To 9
If Sheet1.Cells(i, 5) = Sheet2.Cells(j, 5) Then
chck(i) = True
Exit For
Else: chck(i) = False
End If
Next
Next
j = 0
For i = LBound(chck) To UBound(chck)
If chck(i) = True Then j = j + 1
Next
If j = 5 Then
Sheet1.Cells(5, 6) = 1
Else: Sheet1.Cells(5, 6) = 0
End If
End Sub
Does this really need to be VBA? A formula can perform this calculation. Use this in 'Sheet1' cell F5:
=--(SUMPRODUCT(COUNTIF(Sheet2!E5:E9,E5:E9))>0)
If at least one of the values in 'Sheet1'!E5:E9 (the user entered values) exists in your 'Sheet2'!E5:E9 list, the formula will return a 1 else 0 which is the desired result based on your description.
I've got a macro that works perfectly but that I now need to customize it and add complexity.
The macro is basically the following code repeated numerous times for a variety of ranges.
For i = 2 To n
Range(Cells(13, i), Cells(19, i)).Value = Sheets(i).Range("J13:J19").Value
Next i
The logic/complexity that I need to add to this should go as follows:
if the sum of the range O13:O19 on sheet i is greater than zero, then the value of the range cells(13,i),cells 19,i) on this sheet are equal to the value of the range p13:p19 on sheet i.
If the value of the sum of range O13:O19 on sheet i is not greater than 0, then set the value of the target range equal to each cell in (range sheet(i).range("I13:I19")-sheet(i).range("K13:K19")*4).value
In simpler terms, if the sum of the range is 0, set the value of every cell in range A to the value of every cell in range b less the (value of every cell in range C * 4)...
Sub Op_ex_analysis_macro()
ActiveWorkbook.Sheets.Add Before:=Worksheets(1)
ActiveSheet.Name = "Control Panel"
Range("A:A").ColumnWidth = 36
Range("A12").Value = "Property Code"
Range("A13:A16") = Sheets(2).Range("A13:A16").Value
Range("A17") = Sheets(2).Range("B17").Value
Range("A18") = Sheets(2).Range("A18").Value
Range("A19") = Sheets(2).Range("B19").Value
Range("A20:A29") = Sheets(2).Range("A21:A30").Value
Range("A30") = Sheets(2).Range("B31").Value
Range("A31") = Sheets(2).Range("A33").Value
Range("A32:A36") = Sheets(2).Range("A35:A39").Value
Range("A37:A38") = Sheets(2).Range("A41:A42").Value
Range("A40").Value = "Analyst"
Range("A41").Value = "Number of Units"
Range("A42").Value = "Asset Manager"
Range("A43").Value = "Tenancy"
Range("A44").Value = "Year Built/Type"
Range("A45").Value = "Management Company"
Range("A46").Value = "End of Compliance Year"
Range("A47").Value = "Property Name"
Range("A48").Value = "Number of Properties"
Range("A49").Value = "City"
Range("A50").Value = "State"
'Consolidate Property Codes
n = ActiveWorkbook.Sheets.Count
For i = 2 To n
Z = Sheets(i).Range("P49").Value
Cells(12, i) = Z
Next i
'Consolidate rows 13-19
For i = 2 To n
Select Case Application.Sum(ThisWorkbook.Sheets(i).Range("O13:O33"))
Case Is > 0
Range(Cells(13, i), Cells(19, i)).Value = Sheets(i).Range("P13:P19").Value
Case Is = 0
Range(Cells(13, i), Cells(19, i)).Value = ThisWorkbook.Sheets(i).Range("I13:I19") - ThisWorkbook.Sheets(i).Range("K13:K19") * 4
End Select
Next i
In this case i think the best option is to use a Select case statement.
For i = 2 To n
Select Case Application.Sum(ThisWorkbook.Sheets(i).Range("O13:O33"))
Case Is > 0
Range(Cells(13, i), Cells(19, i)).Value = Sheets(i).Range("P13:P19").Value
Case Is < 0
Range(Cells(13, i), Cells(19, i)).Value = ThisWorkbook.Sheets(i).Range("I13:I19") - ThisWorkbook.Sheets(i).Range("K13:K19") * 4
End Select
Next i
Hope this helps :)
EDIT If ou want to account for whent it's "0" then just add a Case Is 0
After a lot of trial and error, I was able to solve the problem through via a different route.
As A.S.H correctly noted above, you can't do arithmetic on VBA arrays.
The first half of my code was basically moving an array, as Scott Craner noted on a different page, which is simple.
Directing VBA to perform calculations requires the coder to send the formula through a range cell by cell.
Ultimately, the code that performed as required was as follows:
Dim rng As Range
n = ActiveWorkbook.Sheets.Count
With ActiveSheet
For i = 2 To n
If Application.Sum(Sheets(i).Range("O13:O33")) > 0 Then
.Range(.Cells(13, i), .Cells(19, i)).Value = Sheets(i).Range("P13:P19").Value
Else
For Each rng In .Range(.Cells(13, i), .Cells(19, i))
rng.Value = Sheets(i).Cells(rng.Row, "I") - (4 * Sheets(i).Cells(rng.Row, "K"))
Next rng
End If
Next i
End With
If the condition of the first 1/2 of the if statement is met, then it's just set these values equal to those values. If the condition is not met, then the Else statement directs Excel to move through the range performing the calculation as it goes.
I keep getting a "compile error: next without For" when I try to run this code. However, after checking everything over multiple times, I do not see how it does not recognize their presences. This is my first VBA code, so any help would be greatly appreciated.
Sub Naming()
'
' Naming Macro
' Assigns a category name in a cell based on values in a cell one column over
'
Dim number As Double
For i = 9 To 200
number = Cells(i, 3).Value
If number = 0 Then
GoTo Line1
Else
If number <= 199999 And number > 0 Then
Cells(i, 2) = "EP-GEARING"
Else
If number <= 399999 And number > 199999 Then
Cells(i, 2) = "DRIVES"
Else
If number <= 499999 And number > 399999 Then
Cells(i, 2) = "FLOW"
Else
If number <= 599999 And number > 499999 Then
Cells(i, 2) = "SPARES"
Else
If number <= 699999 And number > 599999 Then
Cells(i, 2) = "REPAIR"
Else
If number <= 799999 And number > 699999 Then
Cells(i, 2) = "FS"
Else
If number <= 899999 Then
Cells(i, 2) = "GC-GEARING"
Else
GoTo Line1
Line1:
End If
Next i
End Sub
ElseIf is one word in VB.
If number = 0 Then
'Do nothing
ElseIf number <= 199999 And number > 0 Then
Cells(i, 2) = "EP-GEARING"
ElseIf number <= 399999 And number > 199999 Then
...
Else
'Do nothing
End If
However, Select Case would fit better here:
Select Case number
Case 0
'Do nothing
Case 1 To 199999
Cells(i, 2) = "EP-GEARING"
Case 200000 To 399999
...
Case Else
'Do nothing
End Select
Your code should look like this:
Sub Naming()
'
' Naming Macro
' Assigns a category name in a cell based on values in a cell one column over
'
Dim number As Double
For i = 9 To 200
number = Cells(i, 3).Value
If number <= 199999 And number > 0 Then
Cells(i, 2) = "EP-GEARING"
ElseIf number <= 399999 And number > 199999 Then
Cells(i, 2) = "DRIVES"
ElseIf number <= 499999 And number > 399999 Then
Cells(i, 2) = "FLOW"
ElseIf number <= 599999 And number > 499999 Then
Cells(i, 2) = "SPARES"
ElseIf number <= 699999 And number > 599999 Then
Cells(i, 2) = "REPAIR"
ElseIf number <= 799999 And number > 699999 Then
Cells(i, 2) = "FS"
ElseIf number <= 899999 Then
Cells(i, 2) = "GC-GEARING"
End If
Next i
End Sub
The problem with your code as originally written is that, regardless of the Else clauses, the compiler still expects an End If for every If, and is getting confused because they are not there. The single keyword ElseIf only requires one End If statement at the end.
Goto's are seldom advisable. 99 percent of the time, there's a better and cleaner way to write it, without using a Goto.
The other answers indicate how you could fix your If statement so that VBA recognizes your For and Next pair up.
Now, personally, I would suggest using Select Case as GSerg indicated, if your loop were necessary.
But here is probably what I would do. In Cell B9 place the following formula: =IF(C9=0,"",IF(C9<=199999,"EP-GEARING",IF(C9<=399999,"DRIVES",IF(C9<=499999,"FLOW",IF(C9<=599999,"SPARES",IF(C9<=699999,"REPAIR",IF(C9<=799999,"FS",IF(C9<=899999,"GC-GEARING","")))))))) then copy it down where you need it.
Or if you want to do it with code you could replace your whole sub with no looping I could have written this as a 1 liner, but I wanted it to be legible:
Sub Naming()
'
' Naming Macro
' Assigns a category name in a cell based on values in a cell one column over
Dim theRange As Range
Set theRange = Range(Cells(9, 2), Cells(200, 2))
theRange.Value = "=IF(RC[1]=0,""""," & _
"IF(RC[1]<=199999,""EP-GEARING""," & _
"IF(RC[1]<=399999,""DRIVES""," & _
"IF(RC[1]<=499999,""FLOW""," & _
"IF(RC[1]<=599999,""SPARES""," & _
"IF(RC[1]<=699999,""REPAIR""," & _
"IF(RC[1]<=799999,""FS""," & _
"IF(RC[1]<=899999,""GC-GEARING"",""""))))))))"
'Optional if you want only the values without the formula, uncomment next line
'theRange.Value = theRange.Value
Set theRange = Nothing
End Sub
It is generally faster and cleaner to solve things like this using Excel formulas rather than writing out the logic in VBA and looping through cells.