Using named range in VBA function for VLOOKUP - vba

I have a the following on my worksheet:
A cell that shows a currency [in A1]
A range of cells (two columns, one for the currency, and the other for a corresponding commission percentage) [defined as/named RANGE, and scoped to the worksheet]
A cell that [is trying] to determine the calculated commission percentage based on A1 and RANGE
I then have a VBA function called Calculate, as follows:
Function Calculate(LookupValue As Double, LookupRange As Range) As Double
Calculate = [VLOOKUP(LookupValue, LookupRange, 2)]
End Function
The cell that determines the percentage has the following:
=Calculate(A1, RANGE)
Problem is that the cell just returns #VALUE!...
Any idea what I could be doing wrong?
I have tried several things like type-hinting to Range(), passing LookupRange.Value2 to VLOOKUP, etc, none of which worked.
I have also tried to debug, noting that LookupRange does actually contain the range required in Value2, which is is why I tried to pass it to the function.
Side Note: The function and layout mentioned above is just a dummy - the actual function is somewhat more complex as it relies on negotiated rates, monthly margins, etc. This is why I'm using VBA in the first place. I know that I'm doing something wrong with the lookup, as it is the only thing that seems to be failing within the function - everything else corresponds and calculates.

From MSDN:
The advantage of using square brackets is that the code is shorter. The advantage of using Evaluate is that the argument is a string, so you can either construct the string in your code or use a Visual Basic variable.
in other words you can use
Calculate = [VLOOKUP(3, A1:B100, 2)]
but you can not use
LookupValue = 3
LookupRange = Range("A1:B100")
'or
'LookupRange = "A1:B100"
Calculate = [VLOOKUP(LookupValue, LookupRange, 2)]
What you can do is:
Option 1:
Function Calculate(LookupValue As Double, LookupRange As Range) As Double
Calculate = Evaluate("VLOOKUP(" & LookupValue & "," & LookupRange.Address & ", 2")
End Function
or better:
Function Calculate(LookupValue As Double, LookupRange As Range) As Double
Calculate = Evaluate("VLOOKUP(" & LookupValue & ",'" & _
LookupRange.Parent.Name & "'!" & LookupRange.Address & ", 2")
End Function
However I suggest:
Option 2:
Function Calculate(LookupValue As Double, LookupRange As Range) As Double
Calculate = Application.VLookup(LookupValue, LookupRange, 2)
End Function
I hope you know about meaning of 4th parameter:
If TRUE or omitted, an exact or approximate match is returned. If an exact match is not found, the next largest value that is less than lookup_value is returned.
The values in the first column of table_array must be placed in ascending sort order; otherwise, VLOOKUP may not give the correct value. You can put the values in ascending order by choosing the Sort command from the Data menu and selecting Ascending.
Btw, Calculate is not good name for UDF, since VBA already has function Application.Calculate. I'd rename it to avoid confusion.

Related

Convert string a double in a word macro

I am trying to create a macro for Word 2013 that does the following: the macro should capture the value of a cell of a word table and then add another value and paste the result in another cell of the same table.
My code so far is:
Sub prueba()
Dim a As String, b As String, c As String
Dim entero1 As Double, entero2 As Double
Dim resultado As Double
Dim tabla1 As Table
Set tabla1 = ActiveDocument.Tables(1)
a = tabla1.Cell(Row:=1, Column:=3).Range
entero1 = CDbl(a)
End Sub
But when I run it I get an error 13
To evaluate the error add the following two lines to validate if the data type obtained in "a" was a string
MsgBox (TypeName(a))
MsgBox (a)
And I got the following
I believe that the CDbl function does not finish converting the string to double because as they see the chain has a small square, what is not like to erase it so that the conversion is achieved.
Thank you very much for your help.
One way of extracting just the numeric portion of the Range would be to use the Val function, e.g.
entero1 = Val(a)
If the string a contained, for instance, 123.23XYZ4567 then Val(a) would return the number 123.23.
That should ensure that the non-numeric character that you are getting at the end of your Range is removed.
The answer provided by YowE3K is elegant and has my vote. For further information:
That 'small square' is the end of cell marker which is part of Cell.Range.Text (.Text is the default property returned when returning a range object is inappropriate).
To actually remove the end of cell marker (Chr(13) & Chr(7)) you can use something like this:
?CDbl(Replace$(Selection.Range.Cells(1).Range.Text, Chr(13) & Chr(7), vbNullString))
A possible advantage of this approach is that it may provide better opportunity to trap errors if you are only expecting numeric characters.

(VBA) Custom function refreshing itself with wrong input?

My problem is hard to explain, therefore I added a picture and also shared the sample excel file via my google drive.
What the function should do: Have different item total prices in row "W" and the percentage of transportation costs within the total prices in row "Q" (several other percentage-rows exist for different cost items, this is just to simplify).
Now I SUM the individual item totals.
Then, I apply below function to all percentages in row "Q", which should then give me the total amount of costs, which I then can devide again by row "W" to get the cost percentage of the total.
In my example file, all sub-percentages are equal, but they could as well be different.
The problem that I encounter occurs when I have two sheets with the function below. For some reasons, whenever the function updates on one sheet (and shows the correct value), the function on the other sheet becomes a mess. When I then manually update ("press enter") on the messed-up function, it shows the correct value, but when I go back to the previous sheet, the function is messed up there... I am going crazy : (
And, if I have a third sheet that references "Q" on each sheet, I can never get it to show the correct value for both sheets at the same time, one is always incorrect.
Option Explicit
Dim rCell As Range
Function SUMSubCost(rRange As Range) As Double
Application.Volatile
Dim Total
For Each rCell In rRange
If (Not IsEmpty(rCell)) And (Not IsError(rCell)) Then
Total = Total + (rCell.Value * Range("W" & rCell.Row).Value)
Else
End If
Next rCell
SUMSubCost = Total
End Function
Excel Document
Picture
Without a qualifying worksheet object, your Range("W" & rCell.Row) will always refer to the ActiveSheet, which is not always the one calling your function.
Here's one way to fix it:
Option Explicit
Function SUMSubCost(rRange As Range) As Double
Application.Volatile
Dim Total, rCell As Range
For Each rCell In rRange
If (Not IsEmpty(rCell)) And (Not IsError(rCell)) Then
Total = Total + (rCell.Value * rCell.Worksheet.Range("W" & rCell.Row).Value)
End If
Next rCell
SUMSubCost = Total
End Function

Lookup between two ranges

Afternoon all, I'm working with two columns of data - one of these has an author name and the second has a publish date-time value. I'm looking to build up a UDF to take these two ranges, search the author column for a particular string and if found to return the value from the publish column.
Data looks similar to this;
Once it has found the author I'm looking for i want it to look at the publish time and find the minimum value for this.
For example, if i was looking for the author Ben in the above then the value returned should be 08/06/2014 17:15.
If the data i was working with was always in the same format then i would build up an array formula to create a MINIF but the columns these ranges show in are always different and the easiest option will be a UDF that the end user can just put the two ranges into.
Thanks in advance for any help.
Cheers
Your UDF might be like this, no office 365 features needed:
Function MinDateByAuthor(author As String, rngNames As Range, rngDates As Range) As Date
MinDateByAuthor = Application.Evaluate("Aggregate(15, 6," & _
rngDates.Address(External:=True) & "/(" & _
rngNames.Address(External:=True) & "=""" & author & """),1)")
End Function
You can use it like =MinDateByAuthor("Ben", A2:A100, B2:B100)
You can also place some cell address instead of the hardcoded "Ben".
TBH, all your UDF has done is facilitate a little bit typing the initial formula.
And you might want to make it even easier, by allowing full column references (A:A, B:B) without sensitive slowness. In the above UDF you can do that, but as suggested by #ScottCraner, we can make it work faster:
Function MinDateByAuthor2(author As String, ByVal rngNames As Range, ByVal rngDates As Range) As Date
Set rngNames = Intersect(rngNames, rngNames.Parent.UsedRange)
Set rngDates = Intersect(rngDates, rngDates.Parent.UsedRange)
' The rest is the same ...
MinDateByAuthor2 = Application.Evaluate("Aggregate(15, 6," & _
rngDates.Address(External:=True) & "/(" & _
rngNames.Address(External:=True) & "=""" & author & """),1)")
End Function
Here you will notice that =MinDateByAuthor2(A3,A:A,B:B) calculates faster than =MinDateByAuthor(A3,A:A,B:B).
It's suppose to look like that, just change this row:
If Cells(x,1).Value="ben" Then 'or whatever name you chose
if you want it to be some other Author
Dim x As Integer
Dim y As Integer
Dim a As Date
Application.ScreenUpdating = False
' Set numrows = number of rows of data.
NumRows1 = Range("A1", Range("A1").End(xlDown)).Rows.Count
NumRows2 = Range("B1", Range("B1").End(xlDown)).Rows.Count
Range("A1").Select
For x = 1 To NumRows1
If Cells(x,1).Value="ben" Then 'or whatever name you chose
If IsEmpty(a) Then
A = Cells(x,2)
End If
If DateValue(Cells(x,2))<A Then
A = Cells(x,2)
End If
End If
Next

VBA - Countifs returning 0/Boolean

Having an issue with using Application.Countifs to return a value from a worksheet. Here's the existing method to call it:
Application.CountIfs(sortws.Range(sortws.Cells(1, 3), sortws.Cells(sortws.Rows.Count, 3).End(xlUp)), _
CStr(sortws.Cells(x2, 3)), _
sortws.Range(sortws.Cells(1, 4), sortws.Cells(sortws.Rows.Count, 4).End(xlUp)), _
CStr(sortws.Cells(x2, 4)), _
sortws.Range(sortws.Cells(1, x3), sortws.Cells(sortws.Rows.Count, x3).End(xlUp)), _
Chr(34) & "<>" & Chr(34) & "&0")
In this case the two variables are iteration loops - x2 is equivalent to a row on the spreadsheet, x3 is a column.
When I Print the value, it returns as 0. When I print out the requisite addresses for each range and a string for the last argument, I get this:
=COUNTIFS($C$1:$C$1201,$C$1201,$D$1:$D$1201,$D$1201,$E$1:$E$1201,"<>"&0
Which, as far as I can tell, is an appropriate function. If I copy this out in it's entirety and plug it into a worksheet cell, I get the expected value of 1.
I've printed this formula in the VBA editor using EVALUATE and it returns the same value of 0 as I get with the original formula. I've also tried putting this in a cell next to the data by using this:
sortws.Cells(x2,7).Formula = "=COUNTIFS($C$1:$C$1201,$C$1201,$D$1:$D$1201,$D$1201,$E$1:$E$1201,"<>"&0"
This returns a Boolean value of TRUE, without the formula actually being in the worksheet cell.
If I remove the last bit of the function (that is checking for matches that don't equal 0) then it correctly adds up the total number of matches for the first two columns. As such, I think something is off with my syntax for checking for 0, but I can't determine what it is. All values in that column are numeric (technically currency, but formatted as general since it's just a blank worksheet). I'm struggling to udnerstand why the same formula works when plugged into a cell manually, however.
EDIT: Updating the formula with corrected quotes allows the EVALUATE function to run, as well as putting it in a cell with .Formula. This puts the updated formula as: "=COUNTIFS($C$1:$C$1201,$C$1201,$D$1:$D$1201,$D$1201,$E$1:$E$1201," & chr(34) & "<>" & chr(34) & "&0)", however the issue still remains with the original existing method returning 0, which uses the same method to return cells which don't equal 0 as the corrected quotes formula.
EDIT2: Resolved by #Jeeped, my existing VBA method was trying to replicate what would be required on a worksheet, simplifying the 0 check down to <>0 resolved it.
When supplying quotes within a quoted string, double them up.
sortws.Cells(x2, 7).Formula = "=COUNTIFS($C$1:$C$1201, $C$1201, $D$1:$D$1201, $D$1201, $E$1:$E$1201, ""<>0"")"
The full VBA Excel Application object COUNTIFS function should be written as follows.
Dim n As Long, x2 As Long, x3 As Long, sortws As Worksheet
Set sortws = Worksheets("sheet2")
x2 = 1201
x3 = 5
With sortws
n = Application.CountIfs(.Range(.Cells(1, 3), .Cells(.Rows.Count, 3).End(xlUp)), _
CStr(.Cells(x2, 3)), _
.Range(.Cells(1, 4), .Cells(.Rows.Count, 4).End(xlUp)), _
CStr(.Cells(x2, 4)), _
.Range(.Cells(1, x3), .Cells(.Rows.Count, x3).End(xlUp)), _
"<>0")
Debug.Print n
End With
Reduce your formulas down to the minimum required to avoid string concatenation mistakes and remember to close off your formula strings with a right bracket.

VBA CountIf using R1C1 and text

I'm trying to have a formula display the number or occurrences of the previous row of column "C" (RC3).
An ideal result would look like:
Count: 3
If there were 3 occurrences of the value in the previous row.
The following code returns "False".
y.FormulaR1C1 = "Count: " + "=Application.WorksheetFunction.CountIf(range(R3C3:R" & LR & "C3), " = "&R[-1]C3)"
y is a cell
LR is the last row
Thanks, and let me know if I can clarify further.
The CountIf Function is never going to count anything as long as it's inside a test string. And you don't need 'Application.WorksheetFunction' in a cell formula (only if you want to use that function entirely in VBA). Also you don't need a range function in an Excel formula. Try this:
y.FormulaR1C1 = "=""Count: ""&CountIf(R3C3:R" & LR & "C3,R[-1]C3)"
Though if there is nothing between your search range and your criteria cell, you could eliminate LR altogether with:
y.FormulaR1C1 = "=""Count: ""&CountIf(R3C3:R[-2]C3,R[-1]C3)"
If you are trying to make a formula that combines TEXT and builtin functions, use =CONCATENATE(). consider the example below:
If you are to use VBA to fill in formula with relative location (.FormulaR1C1), it would be:
=CONCATENATE("Count: ",COUNTIF(R[-5]C:R[-2]C,"=" &R[-1]C))