I'm writing a few VBA functions for work and ran into a problem that should be easy to solve, but somehow I can't manage to, despite my best attempts at finding an answer here and on Google. I wrote a function that should give me the range between two strings in a column:
Function FindRng(StartRng As String, EndRng As String) As Variant
Dim TopOfRange As Single
Dim BottomOfRange As Single
TopOfRange = WorksheetFunction.Match(StartRng, Sheets("InfCom").Range("B:B"), 0)
BottomOfRange = WorksheetFunction.Match(EndRng, Sheets("InfCom").Range("B:B"), 0)
FindRng = Range(Sheets("InfCom").Cells(TopOfRange, 2), Sheets("InfCom").Cells(BottomOfRange, 2))
End Function
So if the inputs A and B are on rows 100 and 105, it should return B100:B105. When I test this by adapting the code to read FindRng = Range(...).Address, I indeed get $B$100:$B$105.
However, when I then input the result of FindRng into a customized Index Match function, I get an error. The function is as follows:
Function subsetPBPC(rngReturn As Range, LookupValueH As Variant, TopOfRange As String, BottomOfRange As String, LookupValueV As Variant) As Variant
subsetPBPC = sPBPC(rngReturn, LookupValueH, FindRng(TopOfRange, BottomOfRange), LookupValueV)
End Function
The problem is that it seems to read the output of FindRng not as a range, but as the content of that range: when I use the Evaluate Formula tool on FindRng embedded in another formula, it shows the output of FindRng as {A,B,C,D,E} instead of $B$100:$B$105, where A to E are the contents of the cells in the range. I have the feeling the solution is really simple, but I don't see it. The functions underlying the customized Index Match function have been tested and all work like a charm.
Set instead of let. Let assigns the value of an expression to a variable. Set assigns an object reference to a variable. You want to return a reference to the range object, not return the value produced by the range object's default property.
In VBA writing
FindRng = Range(...)
is implicitly writing
Let FindRng = Range(...)
However you want
Set FindRng = Range(...)
Edit 1:
It is quite important to understand the difference between an object reference and a value in VBA. This is a similar concept to passing arguments by value or by reference. Hopefully these two links help some:
The Let statement on MSDN
The Set statement on MSDN
Edit 2:
Oh, and I guess I should touch on default properties! Some objects like range have default properties. If you treat the range as a value instead of an object, it uses the default property instead of throwing an error because it's an object not a value. In the case of range the default property is Value. So if you say A = Range("A1") what you're actually saying is Let A = Range("A1").Value when you might mean Set A = Range("A1"). So you're getting the value contained in the cell A1, instead of a range object representing that cell.
Picking up that your current code should both
use Set as per AndADM's commnet
dimension SetRng as a Range rather than Variant
you can simplify your function as below (which may save time if you are calling it repetitively)
Also, you could test for this range being Nothing (if your two strings werent found), whereas you current code will error out if either string is missing.
Function SetRng(str1 As String, str2 As String) As Range
With Sheets("infCom").Columns(2)
Set SetRng = Range(.Find(str1, , xlValues, xlWhole), .Find(str2, , xlValues, xlWhole))
End With
End Function
Related
I have a find function, it is supposed to look for "2021" in the range DP.
Range(DP).Find("2021", LookIn:=xlValues, LookAt:=xlWhole).Offset(1, 0) = t
2021 is in range but it is the result of a formula.
(Cell A2 in range DP is =$B$5 and B5=2021)
All ranges and variables are defined and the code works perfectly.
However, every once in while, I get the "'Run-time Error '91'"!!
I press debug, vba page opens and the Find function is the issue. (the value isn't found)
And the best part is, if I delete a random letter of the code and rewrite the EXACT same letter (basically changing NOTHING), and close vba and run again, the code works again without errors!!!
Any idea how to fix this bug?
Thanks in advance.
Make sure to define the worksheet
ThisWorkbook.Worksheets("Sheet1").Range(DP)
Make sure DP is a defined variable (use Option Explicit) or if you meant to use column DP then make sure it is a string Range("DP").
So maybe it should look something like
Dim t As String
t = "test"
Dim DP As String
DP = "A:A"
ThisWorkbook.Worksheets("Sheet1").Range(DP).Find("2021", LookIn:=xlValues, LookAt:=xlWhole).Offset(1, 0) = t
or something like this
ThisWorkbook.Worksheets("Sheet1").Range("DP").Find("2021", LookIn:=xlValues, LookAt:=xlWhole).Offset(1, 0) = "t"
I have essentially a simple syntax question concering Application.Run. I want to write a bit of code where I pass a UDF a string coantaining the name of a worksheet function, e.g. 'iserror' or some other UDF returning boolean. The function will then be exectued for each cell within the passed range and do something depending on result.
However, I have not been able to work out the proper Syntax. Error Messages Change along with my Trials, but non are particularly helpfull. e.g.:
?hrCull(Range("Data!A1:B10"),"Worksheetfunction.iserror", False)
(Error message in German, I'll try my best to translate, but it probably won't 100% match the English Version):
Runtime error 1004:
The macro 'Worksheetfunction.iserror' can not be exectued. The macro may not be available in this worksheet or macros have been deactivated.
Of course, macros have not been deactivated, but it isn't really a macro anyway. Also tried without the leading 'Worksheetfunction', same error message.
In my code the call Looks like this:
Public Function hrCull(r As Range, func As String, Optional invert As Boolean = False) As Range
Dim c As Range
Dim selector As Boolean
...
selector = Application.Run(func, c)
...
end function
I omitted code not relevant.
So what is the proper Syntax?
Misc:
- I'm Aware that I can not assert that the passed function returns a boolean.
- Excel 2016 on Windows 7
A solution using CallByName:
selector = CallByName(Application.WorksheetFunction, "IsError", VbMethod, c)
Lose the WorksheetFunction. prefix, Evaluate doesn't like it as Evaluate is for worksheet functions.
In your function, use:
selector = Application.Evaluate(func & "(" & c.Address & ")")
To test, use:
Debug.Print hrCull(Range("A1"), "ISERROR")
I think you'd be better off declaring your own Enum and adding the functions that you want into this. Then execute them using built in syntax instead of trying to evaluate a string
Public Enum xlSheetFunction
xlIsError
End Enum
Public Function hrCull(r As Range, func As xlSheetFunction, Optional Invert As Boolean = False) As Range
Dim selector As Boolean
Select Case func
Case xlIsError
selector = WorksheetFunction.IsError(r)
End Select
Debug.Print selector
Set hrCull = r
End Function
Public Sub test()
Debug.Print hrCull(Range("A1"), xlIsError)
End Sub
Im writing an If+Or function and would like to use several cell references for the different Logicals in the function, instead of writing each logical statements in the original if+or function. Any ideas of how to solve this? Hope im not too unclear here..
As example: instead of writing =If(or(A1=A2,A3=A4),A1,0) I would like to write out all different logical values in a list of cells, and the just write the original if+or formula like this: =IF(OR(B1),A1,0) where B1 contains the text "A1=A2,A3=A4"
Thanks for any help on this!
You can use the INDIRECT function.
For example, if the value in cell A6 is 10, INDIRECT("A6") = 10.
So basically you can write INDIRECT("A6")=INDIRECT("A7") instead of the A1=A2 condition in your IF formula.
If you want to have "A1=A2" in one cell, you can use LEFT and RIGHT.
Here is an example: https://docs.google.com/spreadsheets/d/157tRicA55TFKKOi86yYBQScnjaQE6fYxaCHFdZx4uUM/edit?usp=sharing
PS: this solution is for Google Sheets so the solution might differ a little if you're using Excel but that should work for Excel too.
You CANNOT Have It All
Instead of using =IF(OR(B1),A1,0) you have to use
e.g. =IFOR(B1,A1) (I prefer "" instead of 0, sorry)
or =IFOR(B1,A1,0) if you (prefer 0 instead of ""),
or change the ElseValue in the declaration to 0,
then you can use =IFOR(B1,A1) to get 0.
Function IFOR(IfOrCell As Range, ThenCell As Range, _
Optional ElseValue As Variant = "", _
Optional SplitDelimiter As String = ",") As Variant
'Description:
'Pending...
'Recalculation
Application.Volatile
'Variables
Dim arrIfOr As Variant
Dim iIfOr As Integer
Dim blnIfOr As Boolean
'The Array: The Split
If InStr(IfOrCell.Value2, SplitDelimiter) = 0 Then Exit Function
arrIfOr = Split(IfOrCell.Value2, SplitDelimiter)
'An Additional Split Introducing the Boolean
For iIfOr = LBound(arrIfOr) To UBound(arrIfOr)
If InStr(arrIfOr(iIfOr), "=") <> 0 Then
If Range(Split(arrIfOr(iIfOr), "=")(0)).Value2 _
= Range(Split(arrIfOr(iIfOr), "=")(1)).Value2 Then
blnIfOr = True
Exit For
End If
End If
Next
'Output
If blnIfOr = True Then
IFOR = ThenCell.Value2
Else
IFOR = ElseValue
End If
End Function
Normally, Dim should be done first and then Set should be done in vba code, like the below code.
Dim xWs1 As Worksheet
Dim xWs2 As Worksheet
.
.
.
Dim xWsN As Worksheet
Set xWs1 = Worksheets("A")
Set xWs2 = Worksheets("B")
.
.
.
Set xWsN = Worksheets("NNN")
If I need to Dim and Set 5 worksheets, 10 lines of code will be needed. It seems that the code will be too long when I need to Dim and Set more than 5 worksheets.
I found that if I just use Set, the vba code can also run properly. I would like to ask this will cause any problems if I didn't use Dim?
Set xWs1 = Worksheets("A")
Set xWs2 = Worksheets("B")
.
.
.
Set xWsN = Worksheets("NNN")
Thanks!
If you don't use dim statement , variable is automatically created as a Variant type.
The Variant type can be an integer, a string, a workbook, or any of the other type of variable and it can change as the variable changes, one moment it can be a string, then it can be changed to a workbook.
Using Dim
Without using Dim
There are mainly two problems with not using Dim,
Variant types uses more computer memory as a result it will make your code slow especially when you use loops.
Difficult to find errors ( in your case you can assign anything to variable XWs1 such as numbers , names etc. which can be avoided if you use dim)
How to Declare and Set 92 Objects using 92 Characters:
If "taking up space" is your concern, and you hypothetically want to explicitly set 5 procedure-level variables (w1..w5) to Set to 5 worksheets (Sht1..Sht5), you could use:
Example #1:
DefObj W
Sub wSet1()
Dim w1, w2, w3, w4, w5
Set w1=[Sht1]:Set w2=[Sht2]:Set w3=[Sht3]:Set w4=[Sht4]:Set w5=[Sht5]
End Sub
...or, even more space-efficient, if for example, you had 92 worksheets to Set in 92 declared variables? Then:
Example #2:
DefObj W
Sub wSet2():Dim wks(1To 92),x:For x=1To 92:Set wks(x)=Sheets("Sht"&x):Next x:End Sub
(That's 92 characters... not counting the line feed!)
Explanation:
Between these two ways of shortening declaration, we're using six shortcuts. Below is a summary of each, and see the links under "More Information" for full documentation on each.
Disclaimer: There are a number of reasons we shouldn't use shortcuts in programming. The obvious one is that, the more you compress code, the harder it is to read and understand (especially by others), and therefore can be harder to troubleshoot or expand upon.
If you don't know what the "standard methods" are, do not learn the shortcuts first! Learn how to do things "THE RIGHT WAY" before learning the shortcuts, no matter how appealing it may seem. There was a time that I argued that neatness like indentation and commenting, and full, proper techniques, didn't matter. I was wrong; had to learn that the hard way. If you're reading this, you'll probably have to learn the hard way too, but at least:
Don't use shortcuts when posting example code in your Stack Overflow questions. (This is not a method of [MCVE]!) You will probably get yelled at! ...and possibly have you questions down-voted or ignored... You were warned!
✓ DefObj (Default Data Types)
[Deftype statements][1] are a forgotten method of declaring default data types. Normally, the default data type is [`Variant`][2], so this:
Dim myVariable as Variant
...is identical to:
Dim myVariable
...however the DefObj W statement (used at module-level) says:
All variables declared in this module, that start with the letter 'W' default to type Object (unless otherwise specified). Note that Deftypes statements must be used at module-level (before your first Sub).
The entire list: (More Info)
DefBool DefByte DefCur DefDate DefDbl DefDec DefInt DefLng DefSng DefStr DefObj DefVar
✓ , (Commas in 'Dim' Statements)
When declaring variables with Dim, multiple variables can be listed on the same line, separated with a comma. Therefore this:
Sub mySub()
Dim myVariable1 as Currency
Dim myVariable2 as Currency
…
...is identical to this: (combining examples with Deftypes)
DefCur m
Sub mySub()
Dim myVariable1, myVariable1
…
✓ Sheets ('Sheets' collection)
The WorkSheets Object refers to the collection of all the Worksheet objects in the specified or active workbook.
The Charts Object` refers to the collection of **all the Chart objects in the specified or active workbook.
But the **Sheets Objectrefers to ***both*** theWorksheets*and*Charts` collections.
So, if a workbook has 3 worksheets and 2 chart sheet, in VBA:
Sheets.Count will return 5
Worksheets.Count will return 3
Warning: Using Sheets could cause a conflict if you have a Chart and a Worksheet with the same name (and should also be avoided when referring to worksheets in other files). But for a simple single-file, worksheet-only workbook, save yourself some Work and stick with just Sheets.
✓ [ ] (Square-Bracket Reference Shortcuts)
[Square brackets] can be used as a shortcut when referring to Worksheets, Cell Ranges and individual Cells. You can use either the A1 Reference Style or a named range within brackets as a shortcut for the Range property. You do not have to type the word "Range" or use quotation marks.
Worksheets("Sheet1").[A1:B5].ClearContents
[MyRange].Value = 30
This is barely documented, and even less documented is the fact that, if used in the logical order, square brackets can be used to refer to worksheets.
Combining examples, all of these statements will have identical result:
Worksheets("Sheet1").Range("A1") = Now()
Sheets("Sheet1").Range("A1") = Now()
Worksheets("Sheet1").[A1] = Now()
Sheets("Sheet1").[A1] = Now()
[Sheet1].[A1] = Now()
✓ wks() (Variable Arrays)
If you have a large number of similar objects to declare, it's often easier (and more organized) to group them together in an array. An array can be declared as any type including, for example, Object, Worksheet. (...or even the rarely-used and bizarre types like LongLong and IConverterApplicationPreferences. (Apparently whoever thought up that one doesn't care for shortcuts.)
✓ For..Set..Next (Loop to Set Variable Arrays)
When using an array of objects (any any variable sets), the next logical step is to reduce code with any tasks that need to be performed on the entire group of objects.
Other Notes:
Example #1 could have been compressed to one line but I wanted it to be easy to read in the answer. If our sheet names were S1..S5 instead of the oh-so-lengthy Sht1..Sht5, and we use the :, we could accomplish the same thing in 105 characters:
Example #1b:
DefObj W
Sub wSet():Dim w1,w2,w3,w4,w5:Set w1=[S1]:Set w2=[S2]:Set w3=[S3]:Set w4=[S4]:Set w5=[S5]:End Sub
Data Type Shortcut Symbols
Another rarely used set of dates back to 1974: data type shortcuts chosen by Gary Kildall for the CP/M Operating System
Symbol Data Type Constant
% Integer vbInteger = 2
$ String vbString = 8
& Long vbLong = 3
# Decimal vbDecimal = 6
! Single vbSingle = 4
# Double vbDouble = 5
Still supported today in many coding languages, you could for example use these interchangeably:
Dim myVariable as String
Dim myVariable$
More Information:
Microsoft.com : How to Break and Combine Statements in Code (VB/VBA)
MSDN : Refer to Cells by Using Shortcut Notation
Excel Hero : Excel VBA Shortcut Range References
MSDN : Using Data Types Efficiently
MSDN : Dim Statement (VBA)
ExcelHowTo : Worksheets vs. Sheets
Stack Overflow : Difference between Worksheets & Worksheet objects
MSDN : Set Statement
MSDN : Declaring Arrays
Take the following example of why using implicit variable declaration is usually a bad idea:
Sub Test()
myVariable = 10
myOutcome = myVaraible + 5
End Test
myOutcome = 5. Can you see why?
I misspelled myVariable in the second line, so I just essentially created a brand new variable myVaraible (which had a default value of 0).
This is why you should always use Option Explicit at the beginning of every module; and why you should always explicitly declare all variables.
While it still works, you are just setting yourself up for needless debugging headaches.
If your issue is that you want to condense your code to use less lines, you can do something like this:
Option Explicit
Sub Test()
Dim myVariable As Long: myVariable = 10
Dim myOutput As Long
myOutput = myVariable + 5
End Sub
You can also declare multiple variables on the same line:
Option Explicit
Sub Test()
Dim myVariable As Long, myOutput As Long
myVariable = 10
myOutput = myVariable + 5
End Sub
Not necessarily recommending this (as it can degrade readability), but it's yet another method of declaring variables. This does require the same data type, but you can add your worksheets in an array (from your example):
Option Explicit
Sub Test()
Dim xWs(1 To 5) As Worksheet
Set xWs(1) = Worksheets("A")
Set xWs(2) = Worksheets("B")
Set xWs(3) = Worksheets("C")
Set xWs(4) = Worksheets("D")
Set xWs(5) = Worksheets("E")
End Sub
I have the following code:
Dim dicMyHash As Dictionary
Dim rngMyRange As Range
' A1 is empty - although the outcome is the same in any case
Set rngMyRange = Range("A1")
Set dicMyHash = New Dictionary
dicMyHash.Add Key:=rngMyRange(1), Item:=0
Debug.Print dicMyHash.Exists(rngMyRange(1).Value) ' returns False
Debug.Print rngMyRange(1) = rngMyRange(1).Value ' returns True
This behavior is somewhat unexpected. Is there some type casting going on in the background? rngMyRange(1).Value property returns a variant, whereas rngMyRange(1) is rngMyRange.item(1), which is a range. However, casting rngMyRange(1) to Variant gives the same results..
Also, adding keys is by value (so a copy of rngMyRange(1) is passed as a key). But still I cannot get why .Exists does not find the key..
Thank you in advance!
So here, we have three different values being passed around:
The original range.
Range.Value, which is a variant.
The copy of (1) which is internal to the dictionary.
If you compare these with equal signs, they are all the same. But according to Dictionary.Exists they are all different.
Why? When you use an equal sign with an object, the equal sign forces the object to call its default property. The default property of Range is Range.Value, which is why r = r.Value and also r = r.Offset(0, 0).
But for a dictionary this isn't so smart. Think about it: Every call to Dictionary.Exists would cause every object used as a key to call its default property. This can get really expensive and it can potentially trigger a lot of side effects. So instead, Dictionary.Exists tests the following:
Are you comparing an object to a non-object? Automatic fail.
Are you comparing two non-ojects? Return a = b.
Are you comparing two objects? Return a Is b.
So r is not the same as r.Value, since one is an object and the other is a non-object. And if you make a copy of r, like with r.Offset(0, 0), those are not the same either since they still point to two different objects, even if the objects have identical contents.
This, on the other hand, will work, since you will make r into the same object as d.Keys(0):
Dim d As Scripting.Dictionary
Dim r As Range
Set r = [a1]
Set d = New Dictionary
d.Add r, 0
Set r = d.Keys(0)
Debug.Print d.Exists(r)
I think the reason of your situation is that rngMyRange is recognised as an two- dimensional array and both array dimensions are passed to your dictionary.
If you change the line which adding element into Dictionary into this one:
dicMyHash.Add Key:=rngMyRange(1).value, Item:=0
it starting to work as you expect- both check points return true.
You could additionally analyse this situation in Locals Window while debugging of your code.
I'm not sure how you are putting this to use, but this will return True:
Sub test()
Dim dicMyHash As Dictionary
Dim rngMyRange As Range
Set rngMyRange = Range("A1")
Set dicMyHash = New Dictionary
dicMyHash.Add Key:=rngMyRange(1).Value, Item:=0 ' assign it with Value
Debug.Print dicMyHash.Exists(rngMyRange(1).Value)
End Sub
So then you'll have an item with a key of whatever's in A1.
I believe the reason it doesn't work without Value is that you are assigning a Range to the Key. It would make more sense to me if you were assigning the range to the Dictionary's Item.