Excel: define a VBA function to shorten PERCENTRANK - excel

In Excel formula become often very long and, consequently, unreadable. For example my formula looks like this:
=PERCENTRANK([vol];[#vol])-PERCENTRANK([pos];[#pos])
I would like to write just
=SCORE("vol", "pos")
How can I create a user defined function from the original worksheet formula?

You can do this relatively easily using VBA's WorksheetFunction. The most difficult part is actually getting the column of the [#Range] so you don't have to enter it - but that's not too complicated either.
In the VBE, create a new standard module and paste the following UDF:
Public Function SCORE(vol As Range, pos As Range) As Double
' We need to get the entire column of the table for each argument
Dim tbl As ListObject, volRng As Range, posRng As Range
Set tbl = vol.ListObject
Rem -> Table columns are relative to the table - not the worksheet
Set volRng = tbl.Range.Columns(vol.Column - tbl.Range.Cells(1).Column + 1)
Set posRng = tbl.Range.Columns(pos.Column - tbl.Range.Cells(1).Column + 1)
With Application.WorksheetFunction
SCORE = .PercentRank(volRng, vol) - .PercentRank(posRng, pos)
End With
End Function
You have to keep in mind that table column numbers are not always the same as the worksheet's (if your table doesn't start in column A). So you essentially have to offset by the number of worksheet columns by subtracting the column from your arguments vol and pos from the column number of the first cell of the entire table tbl.range.cells(1). Then you add one because columns numbers in VBA is "base 1".
You will still have to send the range of the individual cell - so using string arguments are not going to do well here. With all that being said, your new worksheet formula would look like:
=SCORE([#vol],[#pos])
or
=SCORE([#vol];[#pos])
depending on if you separate arguments using , or ;.

Related

VBA - Excel - How to write a formula in a cell instead of values using list objects

I'm struggling with VBA and couldn't find a solution on internet.
This is what I'm trying to do.
I have an excel file with two sheets
First sheet is an extract from a Database (with many columns, an particulary one called "country"). It is formated as a ListObject
Second sheet is a Calculator that is counting the number of occurence of every country in the other sheet. It is also formated as a list object
For Example at the end I'll get:
France: 10
USA: 25
Italia: 30
I found how to calculate this using the Application.WorksheetFunction.CountIf function.
Now my question is: Is it possible to write the formula in the cell, instead of writing the result ?
The objectif is to generate a sheet filled with formulas, so that when I change things in the Database sheet, then it will automatically recalculate my results.
This is my current code: It works, but I'd like to write a formula in the cell, instead of the result.
It there an easyway to do this ?
Thanks a lof for your help !
Regards,
J.
Dim i As Long
Dim CountryName As Variant
Dim KPI_LO As ListObject
Dim REVIEWEDDATA_LO As ListObject
'This is the list object with the database information
Set REVIEWEDDATA_LO = Worksheets(REVIEWEDDATA_SHEET_NAME).ListObjects(REVIEWEDDATA_LISTOBJECT_NAME)
'This is the list object where I'll store my results
Set KPI_LO = Worksheets(WASPKPI_SHEET_NAME).ListObjects(WASPKPI_LISTOBJECT_NAME)
'loop through each country column in the result sheet
For i = LBound(GetCountriesList) To UBound(GetCountriesList)
'Get the name of the first country to find
CountryName = GetCountriesList(i)
'Count the number of occurence of this country in the column called COL_COUNTRY in the revieweddata List Object (in my database sheet).
'This is what I'm trying to replace. I want to write a formula that do this calculation in the cell, instead of the result.
KPI_LO.ListColumns(CountryName).DataBodyRange.Rows(1) = Application.WorksheetFunction.CountIf(REVIEWEDDATA_LO.ListColumns(COL_COUNTRY).DataBodyRange, CountryName)
Next i
if you want to see the formula in a cell, then you can create a function and then just call that and pass through your values.
For example, create a module:
Function fn_Multiply(x As Double, y As Double) As Double
Dim z As Double
z = x * y
fn_Multiply = z
End Function
And use it in excel as a formula like this:
=fn_Multiply(A2,B2)
enter image description here

Sum products of named columns

How do I sum all products of the columns in table with another table?
To make it more clear, look at the image attached. I want the column Cost of table TableA to be equal to
=sum([A]*Lookup([[A];[#Headers]]; Parameters[What]; Parameters[Cost]); ....)
And so on for every column of TableA.
I am however pretty much reluctant of doing it manually and trying to come up with formula to make it automatically, so if I add another column I don't have to modify the formula in column Cost
EDIT
What I have come up so far is something like this:
=sum(
[A]*Lookup([[A];[#Headers]]; Parameters[What]; Parameters[Cost]);
[B]*Lookup([[B];[#Headers]]; Parameters[What]; Parameters[Cost]);
[C]*Lookup([[C];[#Headers]]; Parameters[What]; Parameters[Cost])
)
I want to have a formula that will cover new column if I add one. So, let's say I've added a column named NEW, so the formula should automatically pick it up and effectively work like this:
=sum(
[A]*Lookup([[A];[#Headers]]; Parameters[What]; Parameters[Cost]);
[B]*Lookup([[B];[#Headers]]; Parameters[What]; Parameters[Cost]);
[C]*Lookup([[C];[#Headers]]; Parameters[What]; Parameters[Cost]);
[NEW]*Lookup([[NEW];[#Headers]]; Parameters[What]; Parameters[Cost])
)
The Parameters table will of course include a row with value NEW
If you want it to be (50*4+5*100+1*150) for item 1 then you can apply this formulae in cell B3 assuming that the word "cost is in cell B2
INDEX($B$7:$B$9,MATCH($C$2,$A$7:$A$9,0))*$C3+INDEX($B$7:$B$9,MATCH($D$2,$A$7:$A$9,0))*$D3+INDEX($B$7:$B$9,MATCH($E$2,$A$7:$A$9,0))*$E3
If the parameters in tables TableA and Parameters are in the same order, then you can use the following in column Cost:
=SUMPRODUCT(TRANSPOSE(TableA[#[A]:[C]])*Parameters[Cost])
Entered as array formula (i.e. type the formula then instead of pressing Enter, press Ctrl+Shift+Enter)
EDIT:
If the columns in TableA are not ordered, but those in Parameters are, then you can use the following (I'm not 100% sure it always works, but I've been testing a bit and it seems to work fine):
=SUMPRODUCT(TableA[#[A]:[C]]*LOOKUP(TableA[[#Headers],[A]:[C]],Parameters[What],Parameters[Cost]))
Doesn't need to be entered as array formula.
This answer is amended to take account of your statement that the columns of TableA do not necessarily match entries in the rows of the Parameters table.
If you don't mind using macro code, you could define your own function to do the job:
Option Explicit
Public Function TotalCost(rItemCodes As Range, rCostTable As Range, rItemCounts As Range) As Double
'// this function returns the total cost of an item using
'// a lookup into a table of costs from a code, multiplying
'// the item count by the corresponding cost
Dim rItemCount As Range
Dim rCost As Range
Dim rCode As Range
Dim rMatchingCode As Range
'// Define the code list as the first column of the cost table
Dim rCodeList As Range
Set rCodeList = rCostTable.Columns(1)
TotalCost = 0
Dim ix As Integer
'// Loop through every item code
For ix = 1 To rItemCodes.Columns.Count
Set rItemCount = rItemCounts.Cells(1, ix)
Set rCode = rItemCodes.Cells(1, ix)
Set rMatchingCode = Nothing
On Error Resume Next
'// Find the item that matches in the cost list
Set rMatchingCode = rCodeList.Find(rCode.Value, LookAt:=xlWhole)
'// Check it was found, and calculate the additional cost
If Not rMatchingCode Is Nothing Then
Set rCost = rMatchingCode.Cells(1, 2)
TotalCost = TotalCost + rItemCount.Value * rCost.Value
End If
On Error GoTo 0
Next ix
End Function
If you named the table of costs in the parameters table, say, CostTable (range A8:B10 in your example), and the heading row of table A, say, CodeList (range C1:E1 in your example) then the formula in B2 would be =TotalCost(CodeList,CostTable,C2:E2).

Sum numbers in one cell that contains line break - Excel

I have been searching for few hours and I couldnt find any solution for this problem.
As you can see on the image I have 3 columns and 2 rows. My goal is to sum the cost row (it has line breaks) or split equipment and cost columns into 3 smaller rows. Is this possible?
Something like this in VBA will work:
Function SumLines(ByVal str As String) As Long
Dim arr() As String
arr = Split(str, Chr(10))
For a = 0 To UBound(arr)
SumLines = SumLines + CLng(arr(a))
Next
End Function
However, this will only work if you don't have any characters other than digits and Chr(10)s (new lines).
You then use this in your worksheet, e.g.:
=SumLines(A1)
Unfortunately there's not a particularly clean way to achieve this (to my knowledge). Here are a couple of things you could try though:
Method 1 - worksheet solution
If your first cost cell is in A1 then place the cursor in cell B1 and create a new named range using the following formula:
=EVALUATE(SUBSTITUTE(A1,CHAR(10),"+"))
You can then type the name of the named range into cell B1 and you'll get the sum as expected. Unfortunately you have to create a named range for this because EVALUATE isn't available as a worksheet function - only a named range function and also available in VBA.
I called my named range "eval". You can drag this formula down and it will fill down, always evaluating the cell to it's left.
Method 2 - VBA solution
You can use some simple VBA. Paste this into a new module and then use this formula on the worksheet like this:
=SumAlt(A1)
will return 600 in your example, if A1 contained your 100 200 300
Function SumAlt(s As String) As Long
SumAlt = Evaluate(Replace(s, Chr(10), "+"))
End Function

EXCEL matching elements in a discontinuous range

I want to be able to create a list of matching elements from two columns in different worksheets.
Let me provide a mock example:
I have two lists of elements, in this case boys names and girls names. Those will be placed in different sheets. In the figure below, for simplicity, they are placed in different columns (discontinuos range). I want the formula to generate the list in cells A3:A14 (list all elements in the discontinuous range which match containing the text "jo".
I've learned that the best way to provide such discontinuous range to a formula is by defining a named range.
So far, what I got is:
{IF(
MATCH(
INDEX(Named_Range,
SMALL(IF(Named_Range="*"&$A$3&"*",
ROW(Named_Range)-ROW(INDEX(Named_Range,1,1))+1),ROW()-1)),
'(Names)'!C:C,
0),
INDEX(Named_Range,
SMALL(IF(Named_Range="*"&$A$3&"*",
ROW(Named_Range)-ROW(INDEX(Named_Range,1,1))+1),
ROW()-1)),
"")
}
Named_Range is declared as
=D9:D13,F3:F6
The small function returns the nth element matching the criteria. It should also MATCH one of the names in (Names)!C:C. If so, I get the value of that cell. For the sake of simplicity, consider that '(Names)'!C:C contains all names, so it will always be true.
This formula worked for me using a single column as the range. However, I cannot evaluate the formula using the discontinuous range without Excel crashing, so I'm looking for a piece of advice on how to get it right.
Thanks.
Perhaps the best way to deal with a discontinuous range is to make it continuous...?
Your problem can be simplified a lot with a small VBA function. This function takes a discontinuous named range and returns a single continuous column containing all the same values. That way you can use your INDEX/MATCH/SMALL etc formulas in the normal way
Function Arrange(rng As Range) As Variant
Dim temp As Variant
Dim i As Long
Dim r As Range
ReDim temp(1 To rng.Cells.Count)
i = 1
For Each r In rng
temp(i) = r.Value
i = i + 1
Next r
Arrange = Application.Transpose(temp)
End Function
After you have added this code to a new module in your VBA editor you will be able to use it like this on the worksheet:
=INDEX(ARRANGE(Named_Range),1)
To get the first element, for example.

Excel: Count same number values in noncontiguous range

I'm looking for the most elegant way to count the same number values in a noncontiguous range (I'll refer to it as just 'range'). This is the range:
=$C$2:$C$31,$E$2:$E$31,$G$2:$G$31,$I$2:$I$31,$K$2:$K$31,$M$2:$M$31,$O$2:$O$31,$Q$2:$Q$31,$S$2:$S$7
These are the parameters:
The range contains non-adjacent columns.
The columns differ in height.
The cells in the range are either empty or contain integers.
I'm checking for how many cells equal '1', how many equal '2' etc. in the range. (Not in one go, but in seperate formulas).
I've used a named range to reference the range. I'd really like to use this named range in the formula, in one way or another.
I hope I've given you enough info... Thanks in advance!
I agree with Kartik that a VBA solution is required. However the solution offered is a little inefficient in that it loops over every cell in the ranged passed into the function. It also limits the key parameter to a range reference, and can only count up to 32767 matches. Here's an alternative addresses these shortcomings
Function CountIf_N(rng As Range, key As Variant) As Variant
Dim r As Range
Dim count As Long
count = 0
For Each r In rng.Areas
count = count + WorksheetFunction.CountIfs(r, key)
Next
CountIf_N = count
End Function
Note: assumes Excel 07 or later. If using with an ealier version replace CountIfs with CountIf
One approach is to use excel built in function Countif, but it won't work with non-contigous range. The other way (the easy way) will be to use VBA to create your own custom function, and then use it in excel.
I've presented that technique here.
Goto visual basic editor in excel by pressing Alt+F11, in the project window insert a new module and paste the below code:
Function countif_n(rng As Range, key As Range) As Integer
Dim count As Integer
count = 0
For Each cell In rng
If cell.Value = key.Value Then
count = count + 1
End If
Next cell
countif_n = count
End Function
Here rng is your non-contigous range, and key represent the "range"(cell) which contains the value you want to count. For eg., to check for 1 enter 1 in any cell lets suppose "F2", and your non-contigous range is "testrange"
Then use the above function by entering the following in any blank cell:
=countif_n(testrange, F2)
Although COUNTIF can't handle non-contiguous ranges some functions can, for example RANK and COUNT so for a range called Range this formula will give the number of instances of a number in Z2 within Range
=IFERROR(COUNT(Range)-SUM(RANK(Z2,Range,{1,0}))+2,0)
assumes Excel 2007 or later but can be amended to work in earlier versions
This doesn't quite work if there's stuff below S7 that can't be counted, but you may be able to modify. It also doesn't incorporate the named range.
=SUM(IF(MOD(COLUMN(A2:S31),2)=0,IF(A2:S31=2,1,0)))
This example counts the number of 2's.
This needs to be array-entered with ctrl-shift-enter. It's based on the fact that you're counting in every other column, at least in your example. Also, although you mention the columns are different heights, it looks like all except S are the same height. So maybe there's a way to work around that.

Resources