I am writing a UDF for Excel 2007 which I want to pass a table to, and then reference parts of that table in the UDF. So, for instance my table called "Stock" may look something like this:
Name Cost Items in Stock
Teddy Bear £10 10
Lollipops 20p 1000
I have a UDF which I want to calculate the total cost of all the items left in stock (the actual example is much more complex which can't really be done without very complex formula)
Ideally the syntax of for the UDF would look something like
TOTALPRICE(Stock)
Which from what I can work out would mean the UDF would have the signature
Function TOTALPRICE(table As Range) As Variant
What I am having trouble with is how to reference the columns of the table and iterate through them. Ideally I'd like to be able to do it referencing the column headers (so something like table[Cost]).
This is very basic (no pun intended) but it will do what you describe. For larger tables it may become slow as under the hood it's going back and forth between the macro function and the worksheet, and that kind of activity adds up.
It assumes that you have one row of headers and one column of names (hence the For loop variables starting from 2).
There are all kinds of things that might be necessary - we can save those for another question or another round on this one.
Note that the function returns a "Variant", btw...
Public Function TotalPrice(table As Range) As Variant
Dim row As Long, col As Long
Dim total As Double
For row = 2 To table.Rows.Count
For col = 2 To table.Columns.Count
TotalPrice = TotalPrice + table.Cells(row, col) * table.Cells(row, col + 1)
Next
Next
End Function
Note: I dont have Excel 2007 and I am trying to write this using MSDN doc on the web.
Looks like the range will have ListColumns collection
So, the syntax could be table.ListColumns("Cost").
Does this work?
Related
What ways are there to test an Excel VBA range variable for references to entire columns?
I'm using Excel 2007 VBA, iterating through Range variables with For-Each loops. The ranges are passed into the function as parameters. References to individual cells, ranges of cells, and entire rows are fine.
For instance, these are okiedokie:
Range("A1") 'One cell
Range("A1:D4") 'Range of cells.
Range("10:20") 'Entire rows 10 through 20.
But if any of the ranges have references to entire columns, it will drag the function down to a screeching halt. For instance, these are not okiedokie, and they need to be tested for and avoided:
Range("A:A")
Range("A:Z")
Range("AA:ZZ")
There are a few ways I've throught of to do this, each of them plausible but with weaknesses. The code contains loops which are used for searching through cells in worksheets with many thousands of rows, so speed is critical.
Here are three ways I can think of, but I'd like to know if there are others..?
The simplest & fastest method is to count the rows. If Range(x).Rows.Count=1048576, that's the maximum number of rows in a worksheet. However, this wouldn't work if the actual number of rows turned out to be exactly that number, or if by some wild chance there were multiple overlapping areas/ranges
that all added up to that number. Both unlikely, but possible. Also, if the version of Excel changes, so might that number, thus rendering the code broken.
Use a RegEx match against the text of Range.Address(False,False) with a pattern such as ([A-Z]{1,3}):([A-Z]{1,3}). I think this would be a medium on the speed scale.
Use VBA loops, If-Then, and string functions such as InStr() and Mid() to pick at the text of Range.Address(False,False). I think this would be the slowest possible way to do it.
You could test if the range is a reference to a column by checking the Range.Address against the Range.EntireColumn.Address like this:
If Range("AA:ZZ").Address = Range("AA:ZZ").EntireColumn.Address Then
'This returns True
End If
If Range("AA1:ZZ4").Address = Range("AA1:ZZ4").EntireColumn.Address Then
'This returns False
End If
Not sure I understand the question completely but this might work for you:
Public Sub Test()
Debug.Print RowCheck(ThisWorkbook.Worksheets("Sheet1").Range("A1:A10"))
End Sub
Public Function RowCheck(InputRange As Range)
Dim u As Long 'used number of rows
Dim x As Long 'max number of rows for any column
Dim r As Long 'number of rows based on input range
With InputRange
u = Cells(Rows.Count, .Columns(1).Column).End(xlUp).Row
r = .Rows.Count
x = Rows.Count
End With
If r = x And u < r Then
RowCheck = "A bad column reference provided"
Else
RowCheck = "This is a valid reference"
End If
End Function
Ok, after reading everyone's suggestions, I realized that no matter what I do, any Range objects passed to my function might include either an entire column reference or any combination of overlapping Range references that result in an entire column being selected.
But in translation, that means...all rows in the data, aka the UsedRange. It's possible with a large amount of data the UsedRange may actually hit the last row at 1048576. And any combination of Range references passed to my Function might result in a huge area that does cover an entire column, all the way to the maximum row.
Of course the likelihood of that happening is very low, but I do like to cover all bases in my code. But the key to this puzzle is UsedRange. This creates a "synthetic maximum last row". If the GrandRange, for lack of a better name, covers all rows in the UsedRange, then my function has nothing to do and no data to return. And so a simple IF-Then-Exit should give me the solution I was looking for:
If Intersect(UsedRange,LeGrandeRange).Rows.Count = UsedRange.Rows.Count Then
'All rows in `UsedRange` are affected.
'Nothing to do.
Exit Function
Else
'Do everything here.
'Then exit normally.
...
...
...
Endif
I have written a simple VBA function to look up a value by key in a long table of key/value pairs. It then returns that value as a Double:
Public Function GetValue(sheet As String, key As String) As Double
Dim WS As Worksheet
Dim K As Range
GetValue = 0
Set WS = ThisWorkbook.Worksheets(sheet)
If WS Is Nothing Then Exit Function
Set K = FindKeyAnywhere(WS, key)
If K Is Nothing Then Exit Function
GetValue = WS.Cells(K.Row, 5).Value
End Function
I have about 60 of these formulas in a summary sheet:
=GetValue("Data", B$41)
Where "Data" is the name of the page with key/values, and B$41 is a key. Everything was working perfectly, then...
I fat-fingered the VBA, changing the = 0 to = o and calced. Now every cell on the summary still said #VALUE. When I realized what happened, I fixed the error and recalced. However, every cell on the summary still said #VALUE.
If I click in the cell and hit return, it updated fine.
I checked, autocalculate is turned on. I also clicked calc sheet several times. Nothing.
So I went to every single cell and hit return, so they were all updated. Now they don't say #VALUE, but they still don't update when I change data on the data page.
Is there anything special I'm missing? It seems like Excel is "stuck" thinking the calculation isn't valid.
UPDATE:
Using a named range does not work well, because it has to be typed into every formula. Consider...
KEY1 KEY2 ANOTHERKEY
Data1 =GetValue(A$1,B$1)
Data2
When the user CnPs or drag-fills that formula, it will get the key and sheet automatically. If we use a range name instead, they would have to type in the name in every single cell, and there's hundreds.
Is there a way to take a string and return the named range? Like =Range(A1)
As John Coleman says: Excel does not know it needs to recalculate when something on the Data sheet changes because the precedents of your UDF do not include the range od information on the Data sheet.
You need to:
Either Make the function Volatile - but this has bad recalculation
consequences.
Or pass the range on the Data sheet that contains the information
instead of passing a string. This is the best solution.
Based on your update I think you are looking for INDIRECT, which can convert a string to a range.
But there are major downsides to INDIRECT: its volatile, single-threaded and behaves badly when the string the user gives it does not exist.
IMHO this is not a good direction to go: I would recommend you consider a redesign of your data/algorithms.
Please have a look at this table, which I have named "Tasks":
It is a very basic GANTT chart-like table. Using VBA I use data from this table in order to perform some tasks in other Worksheets.
I have a For loop which loops through each row like this:
For i = 1 To Range("Tasks").Rows.Count
Worksheets("Calendar").Cells(i,2).Value = Range("Tasks").Cells(i,2)
End For
There are many other operations within the For loop, but that goes beyond the scope of my question. Basically what this does is that it loops through the entire table and performs various operations and calculations (where applicable) which results in populating other cells in other worksheets.
My question is this:
Since all columns in the table are labeled, I would like to somehow reference the Column name instead of column number in the loop, if it is possible of course.
For example:
Worksheets("Calendar").Cells(i, 2).Value = Range("Tasks").Cells(i, "Title")
This helps for code readability since then I would know that this references the "Title" column, instead of going back and forth to see which column is e.g. the number "2".
I know that this kind of reference can be done in Excel by using
=Tasks[Title]
(e.g. This expression can be used for Data Validation)
Is it possible to reference columns like this? I am relatevely new to VBA, so I'm not quite sure.
Looking forward for your answer.
if it's an Excel Table, then you can use
Dim lo As ListObject
Dim col As ListColumn
Dim r As Range
Set lo = ActiveSheet.ListObjects("Tasks")
Set col = lo.ListColumns.Item("Title")
r = col.DataBodyRange
For i = 1 To Range("Tasks").Rows.Count
Worksheets("Calendar").Cells(i,2).Value = r.Cells(i)
End For
I have an excel table were A1 = "YES", B1 = "YES" and C1 = "YES". I want to know how many "YES" are in my table, which is easily solved with =COUNTIF(A1:C1,"YES") and it will accurately give me the answer of 3.
A B C
1 YES YES YES
But if want to know how many "YES" are in A1 and C1 and ignore whatever B1 has it becomes tricky. The same function gives me 3 while I want it to give me 2 as an answer since I want to count only A1 and C1.
I want to know if there is a way to manage data so I can work only with that kind of non-consecutive cells? I found that a solution would be using something like =if(A1="YES",1)+if(C1="YES",1) and it works like a charm since it will always give me the right answer; however, that is not a satisfying solution because despite the simplicity in my example, my real situation requires to write around 100 cells from a 500 range for several different combinations which can become somewhat heavy.
I tried using name ranges but it seems the if functions doesn't let me use them the way I want. So any help will be apreciated, thanks.
If VBA is an option and "non-consecutive ranges in excel functions" is the question, then you could create a User Defined Function which takes a ParamArray and returns an array out of all given parameters.
Example
Public Function getMatrix(ParamArray aValues() As Variant) As Variant
Dim i As Long
Dim aReturnValues() As Variant
For Each vValue In aValues
For Each vSingleValue In vValue ' possible the vValue is also an array
ReDim Preserve aReturnValues(i)
aReturnValues(i) = vSingleValue
i = i + 1
Next
Next
getMatrix = aReturnValues
End Function
This could then be used like:
Formula in A4:
=SUMPRODUCT(--(getMatrix(A1,C1,E1:G1,D2:E3)="Yes"))
Note, the UDF is returning an array, not a Range. Thats why we cannot use COUNTIF. COUNTIF needs a Range as first parameter.
Why looking for a formula when you have other features in Excel? Add a row (#2), and have A2=1, B2=0, C2=1 till the last column [Drag it to populate, Make sure to Copy Cells]. Then select row and see the Sum in Status Bar.
Have a look at this thread. Basically what you need is something like this:
=SUM(COUNTIF(INDIRECT({"A1:A10","C1:c10"}),"a"))
Or just use multiple COUNTIFs, and add them. (Countif(...) + countif(...) etc.)
This may be a stupid question, and if it is, I apologise. I have a table in excel:
Column a...........Column b
1 property1.......problem x
2 property2.......problemy
3 property3.......problemz
4 property1......problem a
I was wondering if I could use sumif (or any similar formula) to add the problems, referring to a certain property, in one cell. for ex:
I would have
Column a...........Column b
1 property1.......problem x problem a
The problem is I can't figure out where to start. I tried using sumif but I get an error. Probably because I'm trying to add strings. I tried to mix a vlookup with sumif but that didn't produce anything too. Im stuck here. Thanks for any help!
I am not 100 % sure, but I think you might need to use VBA for this. You could try to create the following custom function:
Create named ranges properties and problems in your sheet.
Click ALT+F11 to open VBA editor.
Press Insert --> Module
Write code
'
Function ConcatIF(Property As String, PropertyRange As Range, ProblemRange As Range) As String
Dim counter As Integer
Dim result As String
result = ""
Dim row As Range
counter = 1
For Each row In PropertyRange.Rows
If (Property = row.Cells(1,1).Value) Then
result = result + ProblemRange.Cells(counter,1).Value + " "
End If
counter = counter +1
Next row
ConcatIF = result
End Function
As I do not have excel on the machine I am writing this on, I could only test it on another machine, and therefore there could be spelling mistakes in this code.
Ensure that you write the code in the module you created, it cannot be written in a Sheet's code, must be module.
This function can then be called as a regular function, like sum, average and if. Create a unique list of all your properties on another sheet. Properties in column A, and then in column B you can call the cuntion. Assume row 1 is used for headings, write the following and copy down.
=ConcatIF(A2,properties,problems)
NOTE!!!! This code gets out of hand very quickly. It needs to do (number of properties) x (number of property/problem pairs) comparisons, so if this number is huge, it could slow down your sheet.
There could be faster methods, but this was from the top of my head.