0

UDF "NAV()" is designed to find the correct report on a network drive based on the first argument (always a date), then loop through all worksheets to find a piece of data with the same row as second argument and same column as third argument (second and third can be text or numbers).

Works reliably in the immediate window. Always returns #VALUE! when used on worksheet, e.g. =NAV(D7,D8,D9) or =NAV(2/19/2016,"Net Asset Value","221-I").

In general it looks like one could get this behaviour if trying to alter other cells in a UDF, but my functions don't do that. Also, I believe all range references specify which workbook and worksheet, so I don't think that is the problem either. I'm not sure where to look next.

Function also attempts to email me a report through Outlook when it fails to find what the user is looking for. I don't know if that is relevant.

Again, what is perplexing is that this code seems to work fine in the immediate window, but only gives #VALUE! when used on a worksheet.

Where else can I look in my code below to determine what would cause NAV() to function correctly in the immediate window, but always yield #VALUE! when used on a worksheet?

Option Explicit

Function NAV(ByVal NAVDate As Date, ByVal matchRow As Variant, ByVal matchColumn As Variant) As Variant
'Application.ScreenUpdating = False
Application.Volatile True

    NAV = FindItemOnWorksheet(NAVDate, matchRow, matchColumn)

'Application.ScreenUpdating = True
End Function


Function FindItemOnWorksheet(ByVal NAVDate As Date, ByVal ItemSpecies As Variant, ByVal ItemGenus As Variant) As Variant
' Finds Item by opening NAV workbook with correct date, activating correct worksheet, and searching for correct row and column
Dim startingRange As Range
Dim ws As Worksheet
Dim wb As Workbook
Dim theDate As Date
Dim theItemSpecies As String
Dim theItemGenus As String

theDate = NAVDate
theItemSpecies = ItemSpecies
theItemGenus = ItemGenus

Set wb = GetWB(NAVDate)

'Loop through ws
Dim WS_Count As Integer
Dim i As Integer

WS_Count = wb.Worksheets.Count

For i = 1 To WS_Count


    Set ws = wb.Worksheets(i)
    Set startingRange = ws.Range("A1:Z100")

    Dim theRow As Range
    Dim theColumn As Range

    Set theRow = startingRange.Cells.Find(theItemSpecies, SearchDirection:=xlPrevious, lookat:=xlWhole)
    If Not (theRow Is Nothing) Then
        Set theColumn = startingRange.Cells.Find(theItemGenus, SearchDirection:=xlPrevious, lookat:=xlWhole)
        If Not (theColumn Is Nothing) Then
            FindItemOnWorksheet = ws.Cells(theRow.Row, theColumn.Column).Value

            wb.Close
            Exit Function
        End If
   End If

Next i
'Loop if no hit on either row or column Find()

'following executes only if no match found
MsgBox "No Match Found. Make sure you are entering arguments--" & vbNewLine & _
        "       The Date of NAV, " & vbNewLine & _
        "       the entry found in the right row of NAV workbooks (e.g. 'Net Asset Value'), " & vbNewLine & _
        "       the right column (e.g. 'Fund')." & vbNewLine & _
        " This function will only find exact matches." & vbNewLine & vbNewLine & _
        "Now emailing developer to ask for a fix."

Dim OutApp As Object
Dim OutMail As Object
Dim strbody As String

Set OutApp = CreateObject("Outlook.Application")
Set OutMail = OutApp.CreateItem(0)

strbody = "User attempted" & _
                "=FindItemOnWorksheet( " & theDate & ", " & theItemSpecies & ", " & theItemGenus & " )" & vbNewLine & _
                "theDate type " & TypeName(theDate) & vbNewLine & _
                "theItemSpecies type " & TypeName(theItemSpecies) & vbNewLine & _
                "theItemGenus type " & TypeName(theItemGenus)

On Error Resume Next
With OutMail
    .To = <Address Removed>
    .CC = ""
    .BCC = ""
    .Subject = "FindItemOnWorksheet Error"
    .Body = strbody
    '.Attachments.Add ("C:\file.xlsx")
    .Send
End With
On Error GoTo 0

Set OutMail = Nothing
Set OutApp = Nothing

FindItemOnWorksheet = "Error"
'wb.Close
Exit Function
End Function


Function GetWB(ByVal NAVDate As Date) As Workbook
'Open requested workbook, return to parent procedure

Dim wbPath As String
Dim wbYear As String
Dim wbMonth As String

Dim wbWeek As String

Dim wbFile As String
Dim wbString As String
Dim wb As Workbook
Dim BackADay As Boolean

Dim OriginalNAVDateRequested As Date
OriginalNAVDateRequested = NAVDate

BackADay = True

'Loop through possible file tree structures and dates to find the closest NAV in the past to the date requested.
Do While BackADay = True

    'Don't go back to a previous week if cannot find current NAV
    If OriginalNAVDateRequested - NAVDate > 4 Then
        BackADay = False
    End If

    wbPath = <Network Path Removed>
    wbYear = CStr(Year(NAVDate)) & "\"
    wbMonth = MonthName(Month(NAVDate)) & " " & wbYear

    wbWeek = DateFormat(NAVDate) & "\"

    wbFile = Dir(wbPath & wbYear & wbMonth & wbWeek & "*Valuation Package*.xlsx")

    'Pricings with distributions have differing tree structure
    If wbFile = "" Then
        wbWeek = wbWeek & "POST Distribution " & wbWeek
        wbFile = Dir(wbPath & wbYear & wbMonth & wbWeek & "*Valuation Package*.xlsx")
        If wbFile = "" Then
            NAVDate = NAVDate - 1
        Else: BackADay = False
        End If
    Else: BackADay = False
    End If

Loop

wbString = wbPath & wbYear & wbMonth & wbWeek & wbFile

Set wb = Workbooks.Open(wbString, UpdateLinks:=False, ReadOnly:=True)
Set GetWB = wb

End Function

Function DateFormat(ByVal X As Date) As String
'Appends leading zeroes if needed to achieve form "00" for any two digit integer, and converts to string
Dim MM As String
Dim DD As String
Dim YYYY As String

If Month(X) < 10 Then
    MM = "0" & CStr(Month(X))
Else
    MM = CStr(Month(X))
End If

If Day(X) < 10 Then
    DD = "0" & CStr(Day(X))
Else
    DD = CStr(Day(X))
End If

YYYY = CStr(Year(X))

DateFormat = MM & "." & DD & "." & YYYY

End Function
  • You should define all the Input arguments as `Variant`. – PatricK Feb 25 '16 at 23:24
  • Thanks, PatricK. I defined all input arguments as `Variant` then tried just removing all type definitions. Both yield the same behaviour. – Micah Neely Feb 25 '16 at 23:31
  • `Variant` inputs type for both `NAV()` and `FindItemOnWorksheet()`? You will need to change code to convert the input data. Try step through it. I think if you do this in immediate window, you get the same error: `?NAV(Range("D7"),Range("D8"),Range("D9"))` – PatricK Feb 25 '16 at 23:37
  • So `=NAV(D7,D8,D9)` on the worksheet still gives '#VALUE!', but `Print NAV(Range("D7"),Range("D8"),Range("D9"))` in the immediate window gives the right answer, just as when I defined the three arguments directly in the immediate window. – Micah Neely Feb 25 '16 at 23:54
  • Have tried to comment out the `Application.Volatile True`? Open up wb and stuff like that may recalc while the first calculation isn't finished (tries to open an open wb) and then fails... it is not a good idea to do "actions" in UDFs at all. – Dirk Reichel Feb 26 '16 at 00:02
  • Also for cases like that just put an `debug.print "start"` as the first action and `debug.print "end"` as the last (also in front of exit function)... if there are 2 starts after each other without an "end" then you are "over looping" the function... (does not happen in direct window, cus it will always executed just once) – Dirk Reichel Feb 26 '16 at 00:35
  • Just wrote test macro and confirmed that Functions triggered in worksheet cannot do Workbooks.Open. You may have to try a different approach. Try use Worksheet Change event, and then call this Function. By the way, the `DateFormat` function can be replaced by built-in `Format(NAVDate,"MM.DD.YYYY")`. – PatricK Feb 26 '16 at 00:40
  • 2
    Functions used as UDF and called from worksheet cells have a number of significant limitations: https://support.microsoft.com/en-us/kb/170787 – Tim Williams Feb 26 '16 at 00:43
  • Thanks for the suggestions, Dirk Reihel and Tim Williams. That along with PatricK specifically testing that worksheet functions cannot open workbooks forces me to conclude that I do need a different approach. A shame since this makes it harder to distribute and a little less flexible, but I will probably need to have a set worksheet format and run it as an event-driven macro. See Patrick's answer below. I will also try to update my code above when I have revised it. – Micah Neely Feb 26 '16 at 20:45

1 Answers1

0

You can Open Workbooks within a Worksheet_Change Event.

For demonstration, if a change in Sheet1!A2, Excel will try open the workbook name with that cell value, then Output the status to Sheet1!A4.

Put below in a Module:

Option Explicit

Function TryOpenWB(ByVal oItem As Variant) As Variant
    Dim sOut As String
    Dim oWB As Workbook
    On Error Resume Next
    Set oWB = Workbooks.Open(CStr(oItem))
    If oWB Is Nothing Then
        sOut = "Cannot open """ & CStr(oItem) & """"
    Else
        sOut = "Opened """ & CStr(oItem) & """ successfully."
        'oWB.Close
    End If
    TryOpenWB = sOut
End Function

Then below in Worksheet Module (I used Sheet1 for demonstration):

Option Explicit

Private Sub Worksheet_Change(ByVal Target As Range)
    If Target.Address = Range("A2").Address Then
        Application.EnableEvents = False
        Range("A4").Value = TryOpenWB(Target)
        Application.EnableEvents = True
    End If
End Sub

So this idea is to open the Workbook only if some cell address is matched.

PatricK
  • 6,375
  • 1
  • 21
  • 25
  • Thanks for sticking with me, PatricK. The real key to solving my problem was just the information that worksheet functions cannot open workbooks, but worksheet events can trigger routines that open workbooks. My code will look a lot different than the above; I will probably make it button-driven; I will update in my original question when revised. – Micah Neely Feb 26 '16 at 20:49