How can I write a UDF in Excel VBA to filter a range of cells? - vba

I've gotten into the habit of marking outlying data by changing cell styles. I'd like to write a UDF in excel to take a Range of cells as input, and return the subset of that range that is not marked as an outlier.
This is what I have tried:
Function ValidCells(rCells As Range) As Range
Dim c As Range
For Each c In rCells
If c.Style <> "Bad" Then
Set ValidCells = Range(c, ValidCells)
End If
Next
End Function
My intent is to be able to do =Sum(ValidCells(A1:D1)), and have it only sum the non-styled data.
However, ValidCells seems to return an empty range every time. What am I doing wrong?

Are you sure it's returning an empty range? When I try running this, VBA raises an error on your 'Set' line. If you're calling the routine as a UDF from the worksheet you won't see the VBA error, but the UDF should stop executing and return #VALUE!.
In any case, you can do what you want, but there is one big caveat. First, the code:
Function ValidCells(rCells As Range) As Range
Dim valid As Range
Dim c As Range
For Each c In rCells
If c.Style <> "Bad" Then
If valid Is Nothing Then
Set valid = c
Else
Set valid = Union(valid, c)
End If
End If
Next
Set ValidCells = valid
End Function
The idea is to build up a multi-area range using VBA's 'Union' method. So, for example, if I put a bad cell in C8, and call ValidCells(B7:D9), this returns the multi-area range $B$7:$D$7,$D$8,$B$8:$B$9,$C$9:$D$9. You can then use the result with SUM just fine.
The caveat is that changing cell styles won't trigger this UDF to recalculate. Normally, you'd be able to add a line like this:
Call Application.Volatile(True)
to your UDF and it would recalc on every change to the workbook. However, it seems like changing a cell style doesn't qualify as a "change" for volatility purposes. So, you can get what you want out of the UDF, but there appears to be no real way to make it work like a "normal" one as far as recalculation goes, even if you mark it as volatile. You'll have to remain aware of that if you use it.

Related

How do I get rid of a circular reference in a VBA dynamic range?

I am trying to create a user-defined Excel Function that, in part, counts all non-blank cells above the cell in which the formula is placed (technically from a specific cell that defines the first cell in the range). The trouble I am having is that copying the formula down is causing a circular reference. I don't want other users to encounter this problem. How can I avoid the circular reference?
I have been trying to solve the problem with:
Set CellOne = Range(“A10”)
Set CellTwo = Range(Selection.Address).Offset(-1, 0)
Set MyRange = Application.Range(Cell1:=CellOne.Address, Cell2:=CellTwo.Address)
CountNonBlanks = Application.WorksheetFunction.CountA(MyRange)
This code also causes the circular reference when copying down:
Set CellTwo = Range(ActiveCell.Address).Offset(-1, 0)
The problem appears to be caused by the reference being relative to which cell is selected or active. I just want MyRange to end one cell above where the formula is placed irrespective of which cell is active or selected.
FWIW, the ultimate purpose of the user-defined formula is to return the next letter in the alphabet no matter how many rows below the prior letter the formula is placed. This native function works, but I was hoping for a more elegant appearing solution:
=MID("abcdefghijklmnopqrstuvwxyz",COUNTA(A$10:A10)+1,1)
Thank you.
You shouldn't be using Selection or Activecell in a worksheet's udf since those are constantly changing. Either pass a range reference into the udf or use application.caller to refer to the cell containing the udf as a range object.
I could edit this response to provide more specific help if you posted the whole udf or at least the declaration. Here's an example.
Public Function nextLetter()
'since no reference is passed in, you might want to make this volatile
Application.Volatile
With Application.Caller.Parent
nextLetter = Chr(97 + Application.CountA(.Range(.Cells(10, "A"), _
.Cells(Application.Caller.Row - 1, "A"))))
End With
End Function
Alternative with starting cell passed in.
Public Function nextLetter2(startRng As Range)
'since only a single cell reference is passed in, you might want to make this volatile
Application.Volatile
With Application.Caller.Parent
nextLetter2 = Chr(97 + Application.CountA(.Range(startRng, _
.Cells(Application.Caller.Row-1, startRng.Column))))
End With
End Function
Use like =nextLetter2(A$10)

Embedded "IF" formula breaks occasionally, VBA alternative?

I have a very large embedded IF formula that appears to occasionally break for no reason. Opening and closing the page a few times eventually gets it working again. I am wondering if there is a VBA alternative for it. Here is the IF formula I am running.
=IF(ISNUMBER(SEARCH("76210",E125)),"_012_00762_10",IF(ISNUMBER(SEARCH("76220",E125)),"_012_00762_20",IF(ISNUMBER(SEARCH("76900",E125)),"_012_00769_00",IF(ISNUMBER(SEARCH("76901",E125)),"_012_00769_01",IF(ISNUMBER(SEARCH("85702",E125)),"_012_00857_02",IF(ISNUMBER(SEARCH("85710",E125)),"_012_00857_10",IF(ISNUMBER(SEARCH("100800",E125)),"_012_01008_00",IF(ISNUMBER(SEARCH("100900",E125)),"_012_01009_00",IF(ISNUMBER(SEARCH("123100",E125)),"_012_01231_00",IF(ISNUMBER(SEARCH("124600",E125)),"_012_01246_00",IF(ISNUMBER(SEARCH("124601",E125)),"_012_01246_01",IF(ISNUMBER(SEARCH("124640",E125)),"_012_01246_40",IF(ISNUMBER(SEARCH("124641",E125)),"_012_01246_41",IF(ISNUMBER(SEARCH("142301",E125)),"_012_01423_01",IF(ISNUMBER(SEARCH("158801",E125)),"_012_01588_01",IF(ISNUMBER(SEARCH("158900",E125)),"_012_01589_00",IF(ISNUMBER(SEARCH("159203",E125)),"_012_01592_03",IF(ISNUMBER(SEARCH("159303",E125)),"_012_01593_03",IF(ISNUMBER(SEARCH("159401",E125)),"_012_01594_01",IF(ISNUMBER(SEARCH("159410",E125)),"_012_01594_10",IF(ISNUMBER(SEARCH("159420",E125)),"_012_01594_20",IF(ISNUMBER(SEARCH("159501",E125)),"_012_01595_01",IF(ISNUMBER(SEARCH("169000",E125)),"_012_01690_00",IF(ISNUMBER(SEARCH("186900",E125)),"_012_01869_00",IF(ISNUMBER(SEARCH("213200",E125)),"_012_02132_00",IF(ISNUMBER(SEARCH("213300",E125)),"_012_02133_00",IF(ISNUMBER(SEARCH("215400",E125)),"_012_02154_00",IF(ISNUMBER(SEARCH("220100",E125)),"_012_02201_00",IF(ISNUMBER(SEARCH("223800",E125)),"_012_02238_00",IF(ISNUMBER(SEARCH("225600",E125)),"_012_02256_00",IF(ISNUMBER(SEARCH("230700",E125)),"_012_02307_00",IF(ISNUMBER(SEARCH("230701",E125)),"_012_02307_01",IF(ISNUMBER(SEARCH("231800",E125)),"_012_02318_00",IF(ISNUMBER(SEARCH("235000",E125)),"_012_02350_00",IF(ISNUMBER(SEARCH("235020",E125)),"_012_02350_20",IF(ISNUMBER(SEARCH("242000",E125)),"_012_02420_00",IF(ISNUMBER(SEARCH("246400",E125)),"_012_02464_00",IF(ISNUMBER(SEARCH("292900",E125)),"_012_02929_00",""))))))))))))))))))))))))))))))))))))))
Basically it is built so a serial number is scanned and it populates a cell for the users who use this sheet with its results from the search. I am already running one macro in this sheet as well. Here is that...
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
Dim rng As Range
Set rng = Intersect(Range("A2:A500, J2:J500"), Target) ' define range of interest
If Not rng Is Nothing Then ' check it's not "nothing"
If WorksheetFunction.CountA(rng) = rng.Count Then 'check for all of its cells being not empty
On Error GoTo safe_exit 'add error control
Application.EnableEvents = False 'don't do anything until you know something has to be done
rng.Offset(, 1).Value = Date 'write Date next to all relevant changed cells
End If
End If
safe_exit:
Application.EnableEvents = True
End Sub
Maybe there is a better way to build this search using a formula that isn't using embedded IF statements, but i couldn't think of another way to do it. Thanks in advance.
This may be what you're looking for:
=IF(ISNA(MATCH(1,IF(ISERR(SEARCH($A$5:$A$42,$E$125)),0,1),0)),"",INDEX($B$5:$B$42,MATCH(1,IF(ISERR(SEARCH($A$5:$A$42,$E$125)),0,1),0)))
entered as an array formula (CTRL-SHIFT-ENTER).
Here $A$5:$A$42 contains 76210, 76220, ... , 292900 (entered as text, not numbers); and $B$5:$B$42 contains _012_00762_10, _012_00762_20, ... , _012_02929_00.
Hope that helps.
Any time you have to go more than 2 deep on an IF you may want to rethink the usage.
What you can do is build a table from your values. Then reference that table as part of your lookup. Assuming your list of value is in range D8:E45 you could use the formula =VLOOKUP(E125,$D$8:$E$45,2).
The beginning of your table would look like what's seen below. The input result cell is referencing your input value and pulling the match of the second column.
To get your table you can take your source formula and replace (Find and Replace - Ctrl+H) some characters with unique delimiting characters. Then use Text To Columns Alt+D+E and delimit and Copy>Paste special>Transpose to quickly have it close to the format you need.

Excel VBA Rows.Count reference for a loop

I need to pull a range of data from a sheet where the top 16 rows will always be the same but the data below will vary. I can find the starting cell with this
Sheets("AA").Next.Select
Range("A17").Select
Selection.End(xlDown).Offset(2, 2).Select
and then I want to count the cells using (starting cell selected above) to end of cells containing data. I have tried this
Range(Selection, Range(Selection.End(xlDown)).Rows.Count
and all kinds of variations on it but cannot seem to make it work. I need to be able refer back to both the start cell and the number of rows to pull data from that cell down to the last cell using a loop (another topic I will probably ask questions on when I get that far)
Can someone help?
Your explanation as to why you are trying to do this makes it appear you are placing more work on yourself than you need to.
Let's create a couple functions.
The first function we will call nextBlankCell, which will automatically grab the last cell before an empty range. In your case, we can even use your selection to determine this - which we will do in our next function.
nextBlankCell Function
Function nextBlankCell(ByVal startRng As Range) As Range
Set nextBlankCell = startRng.End(xlDown)
End Function
Next, let's create a function that will automatically set your entire range for you. In this case, it will be the range from your current selection to the last row containing data that we get from using the above function.
getRngDownwards Function
Function getRngDownwards() As Range
Dim celStart As Range, celEnd As Range, ws As Worksheet
Set celStart = Selection
Set ws = celStart.Parent
Set celEnd = nextBlankCell(celStart)
Set getRngDownwards = ws.Range(celStart, celEnd)
End Function
In the above function, we have two ranges celStart and celEnd. celStart is simply your current selection. I always prefer to immediately set your selection to a static range if you must use Selection (most cases it’s not necessary).
celEnd is the range that will contain the last used cell in your column.
We also determine the worksheet ws by using the selection's parent object. Protip: We avoided ActiveSheet!
Now you can put it to the test:
Sub test()
' This test shows you the address of the range
MsgBox getRngDownwards.Address
' This test visually shows you the range
getRngDownwards.Select
End Sub

Excel VBA: Insheet function code can not access other cells on sheet

I'm having some issues with an insheet function that I am writing in VBA for Excel. What I eventually am trying to achieve is an excel function which is called from within a cell on your worksheet, that outputs a range of data points underneath the cell from which it is called (like the excel function =BDP() of financial data provider Bloomberg). I cannot specify the output range beforehand because I don't know how many data points it is going to output.
The issue seems to be that excel does not allow you to edit cells on a sheet from within a function, apart from the cell from which the function is called.
I have created a simple program to isolate the problem, for the sake of this question.
The following function, when called from within an excel sheet via =test(10), should produce a list of integers from 1 to 10 underneath the cell from which it is called.
Function test(number As Integer)
For i = 1 To number
Application.Caller.Offset(i, 0) = i
Next i
End Function
The code is very simple, yet nothing happens on the worksheet from which this formula is called (except a #Value error sometimes). I have tried several other specifications of the code, like for instance:
Function test(number As Integer)
Dim tempRange As Range
Set tempRange = Worksheets("Sheet1").Range(Application.Caller.Address)
For i = 1 To number
tempRange.Offset(i, 0) = i
Next i
End Function
Strangely enough, in this last piece of code, the command "debug.print tempRange.address" does print out the address from which the function is called.
The problem seems to be updating values on the worksheet from within an insheet function. Could anybody please give some guidance as to whether it is possible to achieve this via a different method?
Thanks a lot, J
User defined functions are only allowed to alter the values of the cells they are entered into, because Excel's calculation method is built on that assumption.
Methods of bypassing this limitation usually involve scary things like caching the results and locations you want to change and then rewriting them in an after calculate event, whilst taking care of any possible circularity or infinite loops.
The simplest solution is to enter a multi-cell array formula into more cells than you will ever need.
But if you really need to do this I would recommend looking at Govert's Excel DNA which has some array resizer function.
Resizing Excel UDF results
Consider:
Public Function test(number As Integer)
Dim i As Long, ary()
ReDim ary(1 To number, 1 To 1)
For i = 1 To number
ary(i, 1) = i
Next i
test = ary
End Function
Select a block of cells (in this case from C1 through C10), and array enter:
=test(10)
Array formulas must be entered with Ctrl + Shift + Enter rather than just the Enter key.

troubles passing a discontinuous named range into a custom function

I've been looking around stack overflow for an answer to this for longer than I care to admit now.
Here's what I have: In a worksheet I have a bunch of discontinuous cells which I need to check for the existence of specific text. I've created a simple function to do this and can do this easily when I define that range manually (in code).
However, when I procedurally create a named range (while doing other stuff) and then try passing in the named range, the function never executes.
I know that the named range is being properly created because I have auto-formatting on it and also I can reference the range with excel formula which accept discontinuous ranges (SUM and whatnot).
Here's the pertinent portions of my code:
Function customProcess1(NamedRange As Range) As Long
For Each c in NamedRange.Cells
...
Next c
End Function
In Excel when I type the formula as "=customProcess1(A1:A2)" I get my number back after the function runs. When I type in "=customProcess1(NamedRange)" my function never even executes.
Again, I'm using the named range as defined already in the document. I can observe the name in the name manager, it references the appropriate cells, i can use the range in formula which accept non-continuous ranges, etc. I can't figure out how to get my working named range into my function.
When I put the formula as "=customProcess1("NamedRange")" the function executes, but since the named range is not ""NamedRange"" but is "NamedRange" it fails to set the object as Range (the object is not found). I've tried taking the named range as a string, but again, if I don't put the quotes around the name, it won't even run the function. So then I've tried passing in a string with the quotes and taking the quotes off inside the function, but this isn't exactly working well either.
In short, I just want to get my non-continuous named range in my custom function. Once I do that, everything is golden.
Anyone have any ideas? I'm not sure why this has been such a chore.
I'm not sure why what you're trying doesn't work and don't really have time to research that part of it, but you could do the following:
Function customProcess1(NamedRange As String) As Long
Dim TheRange As Range
Set TheRange = Range(NamedRange)
For Each c in TheRange.Cells
...
Next c
End Function
Hope this helps.
Adapting your UDF(), I coded:
Function customProcess1(NamedRange As Range) As Long
For Each c In NamedRange.Cells
customProcess1 = customProcess1 + c.Value
Next c
End Function
I then assigned the name Mike to the cells B6,C8,D10 and placed values in these cells. I then placed the formula:
=customProcess1(Mike)
in a cell and got the following:
NOTE:
I did not use =customProcess("Mike")