UDF's on different sheets calling eachother return error 2015 - vba

I have 2 sheets with 3 UDF's in the first and 2 in the second.
sheet 1 is a monthly matrix with 1 column for each day where people put in their hours on the rows beneath. On 3 specific rows there are UDF's that consolidate the data in the column above, referencing the row as an argument. I do the function call like below to avoid having to make the UDF volatile (which prolongs calculation time greatly if I do), so the UDF's result updates when anything changes in column R:
calculateOvertime(R:R)
On sheet 2, the days of the month are in rows (not columns) where one can put in details about their day IF they did overtime. This is detected by one of the UDF's in sheet 1, so the 2 UDF's here require data calculated by a UDF in sheet 1
I have some strange issues with this setup:
For some reason, when I switch tabs, all cells containing any of these functions show up as #VALUE!. I have to add "Application.CalculateFull" to an event handler that fires whenever the tab is activated:
Private Sub Worksheet_activate()
Application.CalculateFull
End Sub
The UDF's that reference a cell containing another UDF on the other tab, will always get '2015!' as a value, referring to error 2015 (a Value error, because the cell contains #VALUE! when the sheet is not active)
Obviously these 2 issues are connected because when I shift sheets, the UDF-calculated values in the non-active sheet are somehow lost.
My method of getting a value from a cell is as follows. I figure out on which row the label in column A is by using the Find() function
Dim compensationRowIndex As Integer
compensationRowIndex = CInt(othersheet.Range("A1:A250").Find("COMPENSATION").Row)
then I get the value and cast it to a Single
Dim compensation As Single
compensation = CSng(othersheet.Cells(compensationRowIndex , columnIndex).Value)
the variable 'compensation' holds the value 2015 always.
Is there any way around this? Also when I want to print the sheets, all cells containing UDF's are filled with #VALUE!. My guess is : If I can make issue 1 go away, so will issue 2.
ANY help on this is much appreciated. I've been troubleshooting this for almost a whole day now and haven't found a solution googling the symptoms.

Problem Solved!
"ActiveSheet" inside a UDF doesn't mean "The Sheet the UDF-containing cell is on" It literally means "the sheet that's active".
When referencing a UDF on anoter sheet, things go horribly wrong. It was all a matter of replacing ActiveSheet with a variable that's set in an If statement that decides from where the UDF is called.
In my case the second sheet always has the word "Info" in it. When on that sheet, you should go one sheet to the left:
Dim ws As Worksheet
If InStr(ActiveSheet.Name, "Info") = 0 Then
Set ws = ActiveSheet
Else
Set ws = Worksheets(ActiveSheet.Index - 1)
End If
Not a 100% waterproof solution (e.g. what if someone reorders the sheets), but for my purposes it's close enough.

The following code snippet (used within a UDF) should do what you want:
Dim ws As Worksheet
If TypeOf Application.Caller Is Range Then
Set ws = Application.Caller.Parent
End If
The Caller property of the Application object points to the Range covering the cell(s) where the UDF was called from. ws will point to the containing worksheet then.
The If TypeOf clause avoids errors in case the function has been called by something else than a UDF (for instance, another VBA procedure), where Caller might not point to a Range object. In such a case, ws remains unassigned (Nothing).

Related

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.

User Inserted Rows/Columns Impacting Excel VBA

I am trying to determine how I can have a user insert columns and/or rows without it impacting the rest of the code in the macro.
Defining names for my objects and using r1c1 references in VBA does not seem to help as these inserted columns shift those references and names as well.
Am I missing something that should be completely obvious???
Or is what I am trying to accomplish not possible?
UPDATE: When I name a range in excel (without VBA) everything seems to work fine with inserted columns. However, when I name the range with VBA everything messes up. Here is a sample of some code to work with.
When this below code is run... I am not able to insert columns as my MSGBOX's don't realize the named cell has shifted to the right. HOWEVER, if I were to remove the first line in this code and just name the cell "GanttStartLocation" which is quoted out in the code... this seems to work fine.
WHY DOES THiS NOT WORK WHEN NAMED WITH VBA????
ActiveWorkbook.Names.Add Name:="DEFINENAMETEST", RefersToR1C1:="=Sheet1!R10C14"
Dim rGanttLocation As Range 'Range used to define where the Gantt chart begins
Dim iFirstRowGantt As Integer 'Defines the first row of the Gantt chart based on rGanttLocation
Dim iFirstColumnGantt As Integer 'Defines the first column of the Gantt chart based on rGanttLocation
'Set rGanttLocation = Worksheets(1).Range("GanttStartLocation")
Set rGanttLocation = Worksheets(1).Range("DEFINENAMETEST")
iFirstRowGantt = rGanttLocation.Row
iFirstColumnGantt = rGanttLocation.Column
MsgBox (iFirstRowGantt)
MsgBox (iFirstColumnGantt)
Use a named range for your cells so that addition of rows/columns are less likely to impact your code if rows/columns are added inside the range. For example: if D1-F10 was called testrange, executing the following subroutine will give red background color to the range
Public Sub Test()
Range("testrange").Interior.Color = vbRed
End Sub
If a new row and column are added to this range, and the subroutine is re-executed after replacing vbRed with vbYellow, the entire range (with new column and row) will turn yellow.
Outside of the named range, it's going to take decent amount of work to keep your Macro's generic, from what I understand.

Excel VBA for hiding cells in one sheet if they match cells in another sheet

I am new to VBA and am having problems learning the rules of variables (I think that's the problem here).
I have two worksheets in a spreadsheet. I need to make code that automatically hides a row on worksheet 2 if that same value in column a is on worksheet 1, column a.
Here's one of the variations of code I've tried:
Dim Sheet2Value As Variant
Dim Sheet1Value As Variant
'
Sheet2Value = Sheets("Sheet2").Range("A:A").Value
Sheet1Value = Sheets("Sheet1").Range("A:A").Value
'
If Sheet2Value = Sheet1Value Then
Sheets("BMAC=N").EntireRow.Hidden = False
Else
Sheets("BMAC=N").EntireRow.Hidden = True
End If
I get a type mismatch error but I'm not sure exactly why. I chose variant because I don't know what I'm doing, but both columns in excel will be set to "General".
Can anyone help with this? What concept am I missing?
Thanks so much for your time.
Few things:
you cannot compare entire column:
Sheet2Value = Sheets("Sheet2").Range("A:A").Value
you need to loop through the collection of cells, see this: Fast compare method of 2 columns
you cannot hide row without defining a range to hide
Sheets("BMAC=N").Range("Some_address").EntireRow.Hidden
Finally, i'd suggest to change your code to shortest way:
Sheets("BMAC=N").Range("A1").EntireRow.Hidden = (value1<>value2)
Good luck!

Excel 2013 upgrade "broke" my VBA function

The attached code is meant as a table of contents builder. Depending on what row the function is on, it plucks certain cells from subsequent worksheets.
Asset Location Model
__________________________________________________________
Freezer Kitchen Freezerator 5000 (from worksheet 2)
Television Den Panasung 55" (from worksheet 3, etc.)
Before the "upgrade", the function just worked. Now, it works if I edit sells, but if I copy cells within the same worksheet (anywhere, not just the "special" cells), the cells flicker and flicker for many seconds and then resolve. Sometimes it fails with an overflow error (if I recall correctly, it didn't do it today.) Copying cells from one worksheet to another just works with no such delay.
If I comment out the Application.Volatile line, the delays go away, but the function does nothing.
Code:
'Return values in subsequent worksheets based on row that the function
'is located on. Usage in this particular instance is a TOC
'Num = number of rows to offset, based on starting position of the TOC
'and skipping any worksheets not to be included in the TOC
'srow and scol is the target cell on the following worksheets
Function GetSpot(num, srow, scol)
Application.Volatile
If num > (ThisWorkbook.Worksheets.Count) Then
GetSpot = " "
Else
GetSpot = ThisWorkbook.Worksheets(num).Cells(srow, scol).Value
End If
If GetSpot = 0 Then GetSpot = " "
End Function
Any ideas?
Thanks in advance.
The issue you're experiencing is a bug in Office 2013 related to user-defined function use within a worksheet. We reported this to Microsoft and were told it will not be made part of a hotfix release as only 3 other people have reported it.
When you have a user-defined function within your worksheet and you perform an action such as copy/paste or insert rows, the worksheet takes painstakingly long to complete calculation. This occurs anytime you have a selection where there are marching ants (for lack of a better term) around the selection and the worksheet with user-defined functions calculates.
It's not your function, it's Office 2013. Here's a quick way to replicate the issue:
Create a new blank workbook
Open Visual Basic Editor and add a new Module
Paste the following VBA in the module
Function TimesTwo(ver)
TimesTwo = ver * 2
End Function
On the workbook, paste the formula =INT(RAND()*8) into cell A1
In the range D1:F100, paste the formula =TimesTwo($A$1)
After configuring the workbook like the above, perform a copy and paste within that worksheet.
The result is that the calculation will take a very long time to complete, which can be seen in the status bar.
There are no workarounds aside from using native Excel functions, if available or working with the Workbook Calculation set to Manual. That is, disabling calculation of the worksheet until you manually invoke it.

Get the cell reference of the value found by Excel INDEX function

The Problem
Assume that the active cell contains a formula based on the INDEX function:
=INDEX(myrange, x,y)
I would like to build a macro that locates the value found value by INDEX and moves the focus there, that is a macro changing the active cell to:
Range("myrange").Cells(x,y)
Doing the job without macros (slow but it works)
Apart from trivially moving the selection to myrange and manually counting x rows y and columns, one can:
Copy and paste the formula in another cell as follows:
=CELL("address", INDEX(myrange, x,y))
(that shows the address of the cell matched by INDEX).
Copy the result of the formula above.
Hit F5, Ctrl-V, Enter (paste the copied address in the GoTo dialog).
You are now located on the very cell found by the INDEX function.
Now the challenge is to automate these steps (or similar ones) with a macro.
Tentative macros (not working)
Tentative 1
WorksheetFunction.CELL("address", ActiveCell.Formula)
It doesn't work since CELL for some reason is not part of the members of WorksheetFunction.
Tentative 2
This method involves parsing the INDEX-formula.
Sub GoToIndex()
Dim form As String, rng As String, row As String, col As String
form = ActiveCell.Formula
form = Split(form, "(")(1)
rng = Split(form, ",")(0)
row = Split(form, ",")(1)
col = Split(Split(form, ",")(2), ")")(0)
Range(rng).Cells(row, CInt(col)).Select
End Sub
This method actually works, but only for a simple case, where the main INDEX-formula has no nested subformulas.
Note
Obviously in a real case myrange, x and ycan be both simple values, such as =INDEX(A1:D10, 1,1), or values returned from complex expressions. Typically x, y are the results of a MATCH function.
EDIT
It was discovered that some solutions do not work when myrange is located on a sheet different from that hosting =INDEX(myrange ...).
They are common practice in financial reporting, where some sheets have the main statements whose entries are recalled from others via an INDEX+MATCH formula.
Unfortunately it is just when the found value is located on a "far" report out of sight that you need more the jump-to-the-cell function.
The task could be done in one line much simpler than any other method:
Sub GoToIndex()
Application.Evaluate(ActiveCell.Formula).Select
End Sub
Application.Evaluate(ActiveCell.Formula) returns a range object from which the CELL function gets properties when called from sheets.
EDIT
For navigating from another sheet you should first activate the target sheet:
Option Explicit
Sub GoToIndex()
Dim r As Range
Set r = Application.Evaluate(ActiveCell.Formula)
r.Worksheet.Activate
r.Select
End Sub
Add error handling for a general case:
Option Explicit
Sub GoToIndex()
Dim r As Range
On Error Resume Next ' errors off
Set r = Application.Evaluate(ActiveCell.Formula) ' will work only if the result is a range
On Error GoTo 0 ' errors on
If Not (r Is Nothing) Then
r.Worksheet.Activate
r.Select
End If
End Sub
There are several approaches to select the cell that a formula refers to...
Assume the active cell contains: =INDEX(myrange,x,y).
From the Worksheet, you could try any of these:
Copy the formula from the formula bar and paste into the name box (to the left of the formula bar)
Define the formula as a name, say A. Then type A into the Goto box or (name box)
Insert hyperlink > Existing File or Web page > Address: #INDEX(myrange,x,y)
Adapt the formula to make it a hyperlink: =HYPERLINK("#INDEX(myrange,x,y)")
Or from the VBA editor, either of these should do the trick:
Application.Goto Activecell.FormulaR1C1
Range(Activecell.Formula).Select
Additional Note:
If the cell contains a formula that refers to relative references such as =INDEX(A:A,ROW(),1) the last of these would need some tweaking. (Also see: Excel Evaluate formula error). To allow for this you could try:
Range(Evaluate("cell(""address""," & Mid(ActiveCell.Formula, 2) & ")")).Select
This problem doesn't seem to occur with R1C1 references used in Application.Goto or:
ThisWorkbook.FollowHyperlink "#" & mid(ActiveCell.FormulaR1C1,2)
You could use the MATCH() worksheet function or the VBA FIND() method.
EDIT#1
As you correctly pointed out, INDEX will return a value that may appear many times within the range, but INDEX will always return a value from some fixed spot, say
=INDEX(A1:K100,3,7)
will always give the value in cell G3 so the address is "builtin" to the formula
If, however, we have something like:
=INDEX(A1:K100,Z100,Z101)
Then we would require a macro to parse the formula and evaluate the arguments.
Both #lori_m and #V.B. gave brilliant solutions in their own way almost in parallel.
Very difficult for me to choose the closing answer, but V.B. even created Dropbox test file, so...
Here I just steal the best from parts from them.
'Move to cell found by Index()
Sub GoToIndex()
On Error GoTo ErrorHandler
Application.Goto ActiveCell.FormulaR1C1 ' will work only if the result is a range
Exit Sub
ErrorHandler:
MsgBox ("Active cell does not evaluate to a range")
End Sub
I associated this "jump" macro with CTRL-j and it works like a charm.
If you use balance sheet like worksheets (where INDEX-formulas, selecting entries from other sheets, are very common), I really suggest you to try it.