I am working with two structured table on two worksheets in one workbook.
I am using my UDF in SheetA which takes 3 arguments as string.
The first argument is ID column, the second argument is the first 8 characters on its column header cell, the third argument is the last 8 characters on its column header cell.
The UDF is applied to many other similar cells. The columns will be expanded as time pass by. ID and header cells are all static value. They are not derived from other cells.
The UDF will search TableB by "Application.Match" and draw values from matching rows and columns. If everything is valid, it will return appropriate results.
SheetA: TableA
ID
04/07/21 - 10/07/21
11/07/21 - 17/07/21
123456
=UDF(TableA:[#[ID]:[ID]],LEFT(TableA[[#Headers],[04/07/21 - 10/07/21]],8),Right(TableA[[#Headers],[04/07/21 - 10/07/21]],8))
=UDF(TableA:[#[ID]:[ID]],LEFT(TableA[[#Headers],[11/07/21 - 17/07/21,8),Right(TableA[[#Headers],[11/07/21 - 17/07/21]],8))
AABBCC
=UDF(TableA:[#[ID]:[ID]],LEFT(TableA[[#Headers],[04/07/21 - 10/07/21]],8),Right(TableA[[#Headers],[04/07/21 - 10/07/21]],8))
=UDF(TableA:[#[ID]:[ID]],LEFT(TableA[[#Headers],[11/07/21 - 17/07/21,8),Right(TableA[[#Headers],[11/07/21 - 17/07/21]],8))
SheetB: TableB
Date
123456
AABBCC
04/07/21
5.0
--
05/07/21
--
7.5
06/07/21
--
7.5
07/07/21
8
--
08/07/21
--
--
09/07/21
--
--
10/07/21
--
--
11/07/21
--
--
My problem is that my UDF is causing time consuming calculation whenever I edit the tables (i.e. volatile) even I'm not editing those three arguments/related cells. Also, UDF recalculate whenever I collapse/expand grouped table columns. I would like to make the UDF recalculate only (when one of the arguments changes) or (if the values in "SumRange" changes). The result should behave somewhat like an index/match formula, when the matching range is updated.
Here shows the UDF:
Function UDF(ID As String, Date1 As String, Date2 As String) As Variant
TargetTable = "TableB"
Set WS1 = ThisWorkbook.Worksheets("SheetB")
With WS1
matchCol = Application.Match(ID, .ListObjects(TargetTable).HeaderRowRange, 0)
If IsError(matchCol) Then
UDF = "ID not found"
Exit Function
End If
matchRow1 = Application.Match(CLng(CDate(Date1)), .ListObjects(TargetTable).ListColumns("Date").Range, 0)
matchRow2 = Application.Match(CLng(CDate(Date2)), .ListObjects(TargetTable).ListColumns("Date").Range, 0)
Set SumRange = .Range(.Cells(matchRow1, matchCol), .Cells(matchRow2, matchCol))
Arr = SumRange.Value
For Each cel In Arr
If Len(Trim(cel)) > 0 Then
UDF = Application.Sum(SumRange)
Exit Function
End If
Next cel
End With
UDF = ""
End Function
I cannot duplicate your problem.
If I change a value in TableB the UDF does NOT recalculate (because it is not volatile and nothing in the UDF argument list has changed).
If I change something in Table A the UDF recalculates because all its fields are either formulas or referenced by the UDF.
Related
I am looking to make a list of heading type 1, sub-heading type 2 and sub-sub-heading type 3, and each subsequent instance of a heading increments in excel. e.g.
Outcome 1
Output 1.1
Activity 1.1.1
Activity 1.1.2
Output 1.2
Activity 1.2.1
Activity 1.2.2
Activity 1.2.3
Outcome 2
Output 2.1
Activity 2.1.1
etc
Here is my formula - getting to be a complicated nested IF statement:
IF([#Column1]="","",
IF([#Column1]="Outcome", "Outcome " & COUNTIF(tbOOA[[#Headers],[Column1]]:[#Column1], [#Column1]),
IF([#Column1]="Output","Output "& COUNTIF(tbOOA[[#Headers],[Column1]]:[#Column1],"Outcome") ***&"."&*** COUNTIF(tbOOA[[#Headers],[Column1]]:[#Column1],[#Column1]),
"Activity " & "serious help")))
In Column 1, choose from a list of 'Outcome', 'Output', or 'Activity'.
In column 2, calculate the appropriate number, e.g. Output 1.2
If the row is empty, then nothing. - Fine
If it is "Outcome", count from the header until current row for the number of instances of "Outcome". - Fine
Else if it is "Output", count the number of "Outcome"s there are. - Fine
This is where it falls apart. Trying to calculate the number after the "." (bold and italic)
I need to count the # of instances of "Output", but then this has to reset to 1 each time there is a new 'Outcome'.
The logic I'm trying to follow is:
(# of "Outputs" from the table header until the current row) minus
(# of "Outputs" from the table header until the last instance of "Outcome")
I've tried several attempts at calculating row number, but everything has been problematic.
The logic is the same for activities, though will complicate the formula even more and I haven't bothered to start on that until I can get level 2 sorted.
Does anyone know of a similar problem/solution?
If you are open to using hidden helper columns, the formulas become much more manageable. Use Column A to hold your "Outcome", "Output", and "Activity" data.
Then use column B to deal with Outcome numbers, column C to deal with Output numbers, and column D to deal with Activity numbers. Merge the final results together in Column E.
In B1, C1, and D1, manually write in the first values (1, 0, and 0).
Then, fill down starting from B2 with the following:
=IF(A2="Outcome",B1+1,B1)
This works by incrementing only if you have found your next Outcome.
Fill down from C2 with the following:
=IF(A2="Outcome",0,IF(A2="Output",C1+1,C1))
This works by incrementing only if you have found your next Output. It resets to 0 if you have a new Outcome.
Then fill down from D2 with
=IF(OR(A3="Outcome",A3="Output"),0,IF(A3="Activity",D2+1,D2))
It's very similar to the prior formula, but resets on an Outcome or an Output.
Finally, in D4, merge it all together with
=B1&IF(C1>0,"."&C1&IF(D1>0,"."&D1,""),"")
& is a string concatenate operation. By checking if the inner values are 0, we only concatenate . and the next number if the next number is non-zero.
I had a similar problem, where I wanted to create the multi-level heading numbers based on the indentation of list of texts. So the numbers must be generated automatically with a user-defined formula (UDF) like below:
For this to work, you must type ="1" in cell A2. The same formula in A3 (below) must be copied down to A4:A14.
=NextLevelNum(A2;IndentLevel(B3))
Function IndentLevel I took from https://professor-excel.com/how-to-return-the-indentation-of-a-cell-in-excel/
Function NextLevelNum I did myself. All code below.
Option Explicit
Public Function IndentLevel(Ref As Range) As Long
Application.Volatile
IndentLevel = Ref.IndentLevel
End Function
Public Function NextLevelNum(prevNumRef As Range, level As Integer) As String
Dim prevNum As String
Dim nums() As String
Dim prevLevel As Integer
prevNum = prevNumRef.Value
nums = Split(prevNum, ".")
prevLevel = UBound(nums) + 1
' Ensure 1 <= level <= prevLevel +1
level = WorksheetFunction.Max(level, 1)
level = WorksheetFunction.Min(level, prevLevel + 1)
ReDim Preserve nums(0 To level - 1)
If level = prevLevel + 1 Then
nums(level - 1) = "1"
Else
nums(level - 1) = CStr(CInt(nums(level - 1)) + 1)
End If
NextLevelNum = Join(nums, ".")
End Function
I'm working on Social Survey project.Due to discrepancies in data I'm stuck at a certain place. The survey conducting volunteers were given tablets with unique IDs. On different dates, the tablets were used in different cities
Sheet 1 one contains a list of around thousands of responses for which city names are missing and Sheet 2 contains a list of tablets in use in different cities on different dates.
Sheet 1
City DeviceID StartDate EndDate
Delhi 25 21-08-2014 26-08-2014
Mumbai 39 14-05-2014 21-05-2014
Chennai 91 17-11-2014 21-11-2014
Bangalore 91 11-10-2014 21-10-2014
Delhi 91 26-05-2015 29-05-2015
Hyderabad 25 23-05-2015 28-05-2015
Sheet 2
S.Id DeviceId SurveyDate City
203 91 15-10-2014 ?
204 25 24-08-2014 ?
I need to somehow fill up the values for the city column in Sheet 2.
I tried using Vlookup but being a beginner to excel, was unable to get things working. I managed to format the string in date columns as date.
But am unsure about how to pursue this further.
From my understanding, Vlookup requires that the date ranges to be continuous, with no missing values in between. It is not so in this case. This is real world data and hence imperfect.
What would be the right approach to this problem ? Can this be done with excel macros ?
I also read up a bit about nested if statements but am confused being a beginner to excel formulas and data manipulation.
There is two ways to do what you want.
The first one is using vba and create a macro to do the job BUT you will have to iterate through all your data multiple time (n1*n2 loops in the worst case scenario where n1 and n2 is the number of rows in it's table respectively) which is really slow if you have a lot of data.
The other way is a little more complicated and includes array formulas but is really faster than vba because it uses the build in functions of excel (which are optimized already).
So I will use a much simpler example and you can use that as you wish on your data.
I have the following tables:
Table1
city ID start end
A 1 3 5
B 3 4 6
C 3 5 8
Table 2
ID point city
3 5 ?
So we want a formula that completes the second table. where ID match exactly and point is between start-end. We are going to use MATCH and INDEX to get it.
Here it is:
=INDEX(A$2:A$4;MATCH(1;(B$2:B$4=G2)*(C$2:C$4<=H2)*(D$2:D$4>=H2);0))
First of all to run this after you write it you should not press enter but instead ctrl+shift+enter to tell excel to run it as an array formula otherwise it will not run at all.
Now we got that out of the way let me explain what is going on here:
The MATCH does the following:
match the value 1 (TRUE) in the range I created and that should be an exact match. But how the range is created? Lets take that part for example:
This B$2:B$4=G2 -gives-> {1;3;3}=3 --> {FALSE;TRUE;TRUE}
Similarly the second thing in the MATCH gives: {TRUE;TRUE;FALSE}
So now we have (keep in mind that the * is similar to logical AND):
{FALSE;TRUE;TRUE}*{TRUE;TRUE;FALSE} --> {FALSE;TRUE;FALSE}
and this combined with the third gives {FALSE;TRUE;FALSE}
So now we have MATCH(1;{FALSE;TRUE;FALSE};0) --> 2 because in the range only the second row matches the 1 (first row that it matches).
So now we just use index to get from another range whatever is on row 2.
You can use the above on your own data to get the expected results.
Good luck!
If the deviceId values should match and the survey date should be between the start date and end date, VLookup won't suffice. The following pointers, however, should get you started:
1) Define the date ranges from which the date comparisons should be made.
2) Use an overlap date checking function to determine if the date in question overlaps the start and end dates.
3) Loop through the date ranges and insert in Sheet2 when a match is found, i.e. when the deviceId values match and the date overlaps.
The following function takes as parameters the date to be checked, the start and end date and returns True, if dateVal overlaps the start and end date:
Function dateOverlap(dateVal As String, startDate As String, endDate As String) As Boolean
If DateDiff("d", startDate, dateVal) >= 0 And DateDiff("d", endDate, dateVal) <= 0 Then _
dateOverlap = True
End Function
Example usage
Debug.Print dateOverlap("05-10-2016", "01-10-2016", "10-10-2016") (returns true).
Here we use MEDIAN() as an easy way to test for "in-between".
Sub FillInTheBlanks()
Dim s1 As Worksheet, s2 As Worksheet
Dim N1 As Long, N2 As Long, i As Long, j As Long
Dim rc As Long, DeId As Long, sDate As Date
Dim wf As WorksheetFunction
Set s1 = Sheets("Sheet1")
Set s2 = Sheets("Sheet2")
Set wf = Application.WorksheetFunction
rc = Rows.Count
N1 = s1.Cells(rc, "A").End(xlUp).Row
N2 = s2.Cells(rc, "A").End(xlUp).Row
For i = 2 To N2
DeId = s2.Cells(i, "B").Value
sDate = s2.Cells(i, "C").Value
For j = 2 To N1
If DeId = s1.Cells(j, 2).Value Then
If sDate = wf.Median(sDate, s1.Cells(j, "C").Value, s1.Cells(j, "D").Value) Then
s2.Cells(i, "D").Value = s1.Cells(j, "A").Value
End If
End If
Next j
Next i
End Sub
Sheet2:
starting from Sheet1:
I want to do an INDEX-MATCH-like lookup between two documents, except my MATCH's index array doesn't stay in one column.
In Vague-English: I want a value from a known column that matches another value that may be found in any column.
Refer to the image below. Let's call everything to the left of the bold vertical line on column H doc1, and the right side will be doc2.
Doc2 has a column "Find This", which will be the INDEX's array. It is compared with "ID1" from doc1 (Note that the values in "Find This" will not be in the same order as column ID1, but it's easier to undertsand this way).
The "[Result]" column in doc2 will be the value from doc1's "Want This" column from the row that matches "FIND THIS" ...However, sometimes the value from "FIND THIS" is not in the "ID1" column, and is instead in "ID2","ID3", etc.
So, I'm trying to generate Col K from Col J. This would be like pressing Ctrl+F and searching for a value in Col J, then taking the value from Col D in that row and copying it to Col K.
I made identical values from a column the same color in the other doc to make it easier to visualize where they are coming from.
Note also that in column F of doc1, the same value from doc2's "Find This" can be found after some other text.
Also note that the column headers are only there as examples, the ID columns are not actually numbered.
I would simply hard-code the correct column to search from, but I'm not in control of doc1, and I'm worried that future versions may have new "ID" columns, with other's being removed.
I'd prefer this to be a solution in the form of a formula, but VB will do.
To generate column K based on given values of column J then you could use the following:
=INDEX(doc1!$D$2:$D$14,SUMPRODUCT((doc1!$B$2:$H$14=J2)*ROW(doc1!$B$2:$H$14))-1)
Copy that formula down as far as you need to go.
It basically only returns the row of the where a matching column J is found. we then find that row in the index of your D range to get your value in K.
Proof of concept:
UPDATE:
If you are working with non unique entities n column J. That is the value on its own can be found in multiple rows and columns. Consider using the following to return the Last row where there J value is found:
=INDEX(doc1!$D$2:$D$14,AGGREGATE(14,6,(doc1!$B$2:$H$14=J2)*ROW(doc1!$B$2:$H$14),1)-1)
UPDATE 2:
And to return the first row where what you are looking in column J is found use:
=INDEX($D$2:$D$14,AGGREGATE(15,6,1/($B$2:$H$14=J2)*ROW($B$2:$H$14)-1,1))
Thanks to Scott Craner for the hint on the minimum formula.
To determine if you have UNIQUE data from column J in your range B2:H14 you can enter this array formula. In order to enter an array formula you need to press CTRL+SHFT+ENTER at the same time and not just ENTER. You will know you have done it right when you see {} around your formula in the formula bar. You cannot at the {} manually.
=IF(MAX(COUNTIF($B$2:$H$14,J2:J14))>1,"DUPLICATES","UNIQUE")
UPDATE 3
AGGREGATE - A relatively new function to me but goes back to Excel 2010. Aggregate is 19 functions rolled into 1. It would be nice if they all worked the same way but they do not. I think it is functions numbered 14 and up that will perform the same way an array formula or a CSE formula if you prefer. The nice thing is you do not need to use CSE when entering or editing them. SUMPRODUCT is another example of a regular formula that performs array formula calculations.
The meat of this explanation I believe is what is happening inside of the AGGREGATE brackets. If you click on the link you will get an idea of what the first two arguments are. The first defines which function you are using, and the second tell AGGREGATE how to deal with Errors, hidden rows, and some other nested functions. That is the relatively easy part. What I believe you want to know is what is happening with this:
(doc1!$B$2:$H$14=J2)*ROW(doc1!$B$2:$H$14)
For illustrative purpose lets reduce this formula to something a little smaller in scale that does the same thing. I'll avoid starting in A1 as that can make life a little easier when counting since it the 1st row and first column. So by placing the example range outside of it you can see some more special considerations potentially.
What I want to know is what row each of the items list in Column C occurs in column B
| B | C
3 | DOG | PLATYPUS
4 | CAT | DOG
5 | PLATYPUS |
The full formula for our mini example would be:
{=($B$3:$B$5=C2)*ROW($B$3:$B$5)}
And we are going to look at the following as an array
=INDEX($B$3:$B$5,AGGREGATE(14,6,($B$3:$B$5=C2)*ROW($B$3:$B$5),1)-2)
So the first brackets is going to be a Boolean array as you noted. Every cell that is TRUE will TRUE until its forced into a math calculation. When that happens, True becomes 1 and False becomes 0.I that formula was entered as a CSE formula and place in D2, it would break down as follows:
FALSE X 3
FALSE X 4
TRUE X 5
The 3, 4 and 5 come from ROW() returning the value of the row number that it is dealing with at the time of the array math operation. Little trick, we could have had ROW(1:3). Just need to make sure the size of the array matches! This is not matrix math is just straight across multiplication. And since the Boolean is now experiencing a math operation we are now looking at:
0 X 3 = 0
0 X 4 = 0
1 X 5 = 5
So the array of {0,0,5} gets handed back to the aggregate for more processing. The important thing to note here is that it contains ONLY 0 and the individual row numbers where we had a match. So with the first aggregate formula, formula 14 was chosen which is the LARGE function. And we also told it to ignore errors, which in this particular case does not matter. So after providing the array to the aggregate function, there was a ,1) to finish off the aggregate function. The 1 tells the aggregate function that we want the 1st larges number when the array is sorted from smallest to largest. If that number was 2 it would be the 2nd largest number and so on. So the last row or the only row that something is found on is returned. So in our small example it would be 5.
But wait that 5 was buried inside another function called Index. and in our small example that INDEX formula would be:
=INDEX($B$3:$B$5,AGGREGATE(...)-2)
Well we know that the range is only 3 rows long, so asking for the 5th row, would have excel smacking you up side the head with an error because your index number is out of range. So in comes the header row correction of -1 in the original formula or -2 for the small example and what we really see for the small example is:
=INDEX($B$3:$B$5,5-2)
=INDEX($B$3:$B$5,3)
and here is a weird bit of info, That last one does not become PLATYPUS...it becomes the cell reference to =B5 which pulls PLATYPUS. But that little nuance is a story for another time.
Now in the comments Scott essentially told me to invert for the error to get the first row. And this is important step for the aggregate and it had me running in circles for awhile. So the full equation for the first row option in our mini example is
=INDEX($B$3:$B$5,AGGREGATE(15,6,1/($B$3:$B$5=C2)*ROW($B$3:$B$5),1)-2)
And what Scott Craner was actually suggesting which Skips one math step is:
=INDEX($B$3:$B$5,AGGREGATE(15,6,ROW($B$3:$B$5)/($B$3:$B$5=C2),1)-2)
However since I only realized this after writing this all up the explanation will continue with the first of these two equations
So the important thing to note here is the change from function 14 to function 15 which is SMALL. Think of it a finding the minimum. And this time that 6 plays a huge factor along with the 1/. So our array in the middle this time equates to:
1/FALSE X 3
1/FALSE X 4
1/TRUE X 5
Which then becomes:
1/0 X 3
1/0 X 4
1/1 X 5
Which then has excel slapping you up side the head again because you are trying to divide by 0:
#div/0 X 3
#div/0 X 4
1/1 X 5
But you were smart and you protected yourself from that slap upside the head when you told AGGREGATE to ignore error when you used 6 as the second argument/reference! Therefore what is above becomes:
{5}
Since we are performing a SMALL, and we passed ,1) as the closing part of the AGGREGATE, we have essentially said give me the minimum row number or the 1st smallest number of the resulting array when sorted in ascending order.
The rest plays out the same as it did for the LARGE AGGREGATE method. The pitfall I fell into originally is I did not use the 1/ to force an error. As a result, every time I tried getting the SMALL of the array I was getting 0 from all the false results.
SUMPRODUCT works in a very similar fashion, but only works when your result array in the middle only returns 1 non zero answer. The reason being is the last step of the SUMPRODUCT function is to all the individual elements of the resulting array. So if you only have 1 non zero, you get that non zero number. If you had two rows that matched for instance 12 and 31, then the SUMPRODUCT method would return 43 which is not any of the row numbers you wanted, where as aggregate large would have told you 31 and aggregate small would have told you 12.
Something like this maybe, starting in K2 and copied down:
=IFERROR(INDEX(D:D,MAX(IFERROR(MATCH(J2,B:B,0),-1),IFERROR(MATCH(J2,E:E,0),-1),IFERROR(MATCH(J2,G:G,0),-1),IFERROR(MATCH(J2,H:H,0),-1))),"")
If you want to keep the positions of the columns for the Match variable, consider creating generic range names for each column you want to check, like "Col1", "Col2", "Col3". Create a few more range names than you think you will need and reference them to =$B:$B, =$E:$E etc. Plug all range names into Match functions inside the Max() statement as above.
When columns are added or removed from the table, adjust the range name definitions to the columns you want to check.
For example, if you set up the formula with five Matches inside the Max(), and the table changes so you only want to check three columns, point three of the range names to the same column. The Max() will only return one result and one lookup, even if the same column is matched several times.
I came up with a vba solution if I understood correctly:
Sub DisplayActiveRange()
Dim sheetToSearch As Worksheet
Set sheetToSearch = Sheet2
Dim sheetToOutput As Worksheet
Set sheetToOutput = Sheet1
Dim search As Range
Dim output As Range
Dim searchCol As String
searchCol = "J"
Dim outputCol As String
outputCol = "K"
Dim valueCol As String
valueCol = "D"
Dim r As Range
Dim currentRow As Integer
currentRow = 1
Dim maxRow As Integer
maxRow = sheetToOutput.UsedRange.Rows.Count
For currentRow = 1 To maxRow
Set search = Range("J" & currentRow)
For Each r In sheetToSearch.UsedRange
If r.Value <> "" Then
If r.Value = search.Value Then
Set output = sheetToOutput.Range(outputCol & currentRow)
output.Value = sheetToSearch.Range(valueCol & currentRow).Value
currentRow = currentRow + 1
Set search = sheetToOutput.Range(searchCol & currentRow)
End If
End If
Next
Next currentRow
End Sub
There might be better ways of doing it, but this will give you what you want. We assume headers in both "source" and "destination" sheets. You will need to adapt the "Const" declarations according to how your sheets are named. Press Control & G in Excel to bring up the VBA window and copy and paste this code into "This Workbook" under the "VBA Project" group, then select "Run" from the menu:
Option Explicit
Private Const sourceSheet = "Source"
Private Const destSheet = "Destination"
Public Sub FindColumns()
Dim rowCount As Long
Dim foundValue As String
Sheets(destSheet).Select
rowCount = 1 'Assume a header row
Do While Range("J" & rowCount + 1).value <> ""
rowCount = rowCount + 1
foundValue = FncFindText(Range("J" & rowCount).value)
Sheets(destSheet).Select
Range("K" & rowCount).value = foundValue
Loop
End Sub
Private Function FncFindText(value As String) As String
Dim rowLoop As Long
Dim colLoop As Integer
Dim found As Boolean
Dim pos As Long
Sheets(sourceSheet).Select
rowLoop = 1
colLoop = 0
Do While Range(alphaCon(colLoop + 1) & rowLoop + 1).value <> "" And found = False
rowLoop = rowLoop + 1
Do While Range(alphaCon(colLoop + 1) & rowLoop).value <> "" And found = False
colLoop = colLoop + 1
pos = InStr(Range(alphaCon(colLoop) & rowLoop).value, value)
If pos > 0 Then
FncFindText = Mid(Range(alphaCon(colLoop) & rowLoop).value, pos, Len(value))
found = True
End If
Loop
colLoop = 0
Loop
End Function
Private Function alphaCon(aNumber As Integer) As String
Dim letterArray As String
Dim iterations As Integer
letterArray = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
If aNumber <= 26 Then
alphaCon = (Mid$(letterArray, aNumber, 1))
Else
If aNumber Mod 26 = 0 Then
iterations = Int(aNumber / 26)
alphaCon = (Mid$(letterArray, iterations - 1, 1)) & (Mid$(letterArray, 26, 1))
Else
'we deliberately round down using 'Int' as anything with decimal places is not a full iteration.
iterations = Int(aNumber / 26)
alphaCon = (Mid$(letterArray, iterations, 1)) & (Mid$(letterArray, (aNumber - (26 * iterations)), 1))
End If
End If
End Function
I have a UDF which reads data from other sheets indirectly, adding the same cell across a number of sheets, ie:
Function myFunction( StrArgs As String ) As Long
....
End Function
I'm calling this function from cell A1, which right now has a value of 100:
=myFunction( ... )
Calculation is set to manual, and the sheet is refreshed when needed.
Since the arguments StrArgs define the sheets to be queried, I'm including some error-checking inside myFunction in case either the specified sheets don't exist, or there's an error in the specification of StrArgs.
This works fine, however what I'm struggling with is the following: when an error is found within myFunction, I want to return (keep) the existing value of the calling cell, instead of a zero or error value.
What I want to do at the beginning of myFunction is:
existingCellValue = Application.Caller.Text 'or Application.Caller.Value
Then perform the calculations, and when an error is encountered:
myFunction = existingCellValue
However, I find that this returns zero. In the debugger I see that, as soon as myFunction starts to execute, the cell value is already set to zero.
My question is - is there a way to access the existing value of the calling cell, before executing the UDF ?
-- edit - more complete code, as an example, which seems to work fine: --
Function GETNUMBER(Col As String, Row As Integer) As Double
Dim LookStr As String
Dim TheAnswer As Double
Dim CellVal As Variant
On Error GoTo errHandler
CellVal = Application.Caller.Text
LookStr = "=" & Col & Row
TheAnswer = Application.Evaluate(LookStr)
GETNUMBER = TheAnswer
On Error GoTo 0
Exit Function
errHandler:
GETNUMBER = CellVal
End Function
With the above code in a module, I enter the following in my workbook:
Row
1: | D | 1 | =GETNUMBER(A1,B1) | 10
2: | D | 2 | =GETNUMBER(A2,B2) | 20
3: | D | 3 | =GETNUMBER(A3,B3) | 30
This returns the values of 10, 20 and 30 from column D.
Now I change one of the cells in column B to zero, to invoke the errHandler, and return CellVal, stored at the start.
This seems to work, and both Application.Caller.Text and Application.ThisCell.Text give the correct result.
Thanks to both Charles Watson and KazJaw, both of whom answered the question.
There are several possible ways, but they all have disadvantages. The simplest way is to use Application.Caller.Text but it returns the formatted value rather than the actual value.
See my blog post on the subject for more discussion http://fastexcel.wordpress.com/2012/01/08/writing-efficient-vba-udfs-part-8-getting-the-previously-calculated-value-from-the-calling-cells/
You can do it with Application.ThisCell property. See quite simple example below:
Public Function MultipleAB(a, b)
Debug.Print Application.ThisCell.Text 'here you get current value
MultipleAB = a * b 'here you get new value
End Function
Important!! Application.ThisCell is valid only for UDFs used in cells. When used in other VBA Subs can return errors.
The picture below presents how this solution works (for randomly changed values in cells C1 & C2):
I'm a bit new to trying to program and originally was just trying to improve a spreadsheet but it's gone beyond using a basic function in excel. I have a table that I am having a function look at to find a building number in the first column and then look at start and finish dates in two other respective columns to find out if it should populate specific blocks on a calendar worksheet. The problem occurs because the same building number may appear multiple times with different dates and I need to to find an entry that matches the correct dates.
I was able to create a working though complicated formula to find the first instance and learned I can add a nested if of that formula again in the false statement with a slight change. I can continue doing that but it becomes very large and cumbersome. I'm trying to find a way to make a function for the formula with a variable in it that would look at how many times the it has already been used so it keeps searching down the table for an answer that fits the parameters.
This is currently my formula:
=IFERROR(IF(AND(DATE('IF SHEET (2)'!$F$7,MATCH('IF SHEET (2)'!$C$2,'IF SHEET (2)'!$C$2:'IF SHEET (2)'!$N$2,0),'IF SHEET (2)'!C$4)>=VLOOKUP("2D11"&1,A2:F6,4,0),DATE('IF SHEET (2)'!$F$7,MATCH('IF SHEET (2)'!$C$2,'IF SHEET (2)'!$C$2:'IF SHEET (2)'!$N$2,0),'IF SHEET (2)'!C$4)<=VLOOKUP("2D11"&1,A2:F6,4,0)),IF(VLOOKUP("2D11"&1,A2:F6,3,0)="2D11",VLOOKUP("2D11"&1,A2:F6,6,FALSE)),"NO ANSWER"),"ERROR")
Where you see 2D11&1 is where I need the variable for 1 so it would be "number of times it's been used in the function +1" then I could just loop it so it would keep checking till it ran out of 2D11's or found one that matched. I haven't posted before and I'm doing this through a lot of trial and error so if you need more info please post and say so and I'll try to provide it.
So rather than have someone try to make sense of the rediculous formula I posted I though I would try to make it simpler by just stating what I need to accomplish and trying to see how to turn that into a VBA function. So I'm kinda looking at a few steps:
Matches first instance of building name in column A with
building name for the row of the output cell.
Is date connected with the output cell >= start date of first entry(which is user entered in column D).
Is date connected with the output cell <= end date of first entry(which is user entered in column E).
Enters Unit name(located in column F) for first instance of the building if Parts 1, 2, and 3 are all True.
If parts 1, 2, or 3 are False then loops to look at next instance of the building name down column 1.
Hopefully this makes things clearer than the formula so I'm able to get help as I'm still pretty stuck due to low knowledge of VBA.
Here is a simple solution...
Building_name = ???
Date = ???
Last_Row = Range("A65536").End(xlUp).Row
For i = 1 To Last_Row
if cells(i,1).value = Building_Name Then
if date >= cells(i,4).value Then
if date <= cells(i,5).value Then
first instance = cells(i,6).value
end if
end if
end if
next
you should add a test at the end to avoid the case where there is no first instance in the table
If I understand correctly, you have a Table T1 made of 3 columns: T1.building, T1.start date, T1.end date.
Then you have 3 parameters: P1=building, P2=start date, P3=end date.
You need to find the first entry in table T1 that "fits" within the input parameters dates, that is:
P1=T1.building
P2<=T1.start date
P3>=T1.end date
If so, you can define a custom function like this
Public Function MyLookup(Key As Variant, DateMin As Variant, DateMax As Variant, LookUpTable As Range, ResultColumn As Integer) As Range
Dim iIndx As Integer
Dim KeyValue As Variant
Dim Found As Boolean
On Error GoTo ErrHandler
Found = False
iIndx = 1
Do While (Not Found) And (iIndx <= LookUpTable.Rows.Count)
KeyValue = LookUpTable.Cells(iIndx, 1)
If (KeyValue = Key) And _
(DateMin <= LookUpTable.Cells(iIndx, 2)) And _
(DateMax >= LookUpTable.Cells(iIndx, 3)) Then
Set MyLookup = LookUpTable.Cells(iIndx, ResultColumn)
Found = True
End If
iIndx = iIndx + 1
Loop
Exit Function
ErrHandler:
MsgBox "Error in MyLookup: " & Err.Description
End Function
That may not be the most performant piece of code in the world, but I think it's explanatory.
You can download this working example