I have a workbook, UserFileBook, that contains the Name 'Version' which simply refers to a number (it does not refer to any range, in the Name Manager it just 'Refers to =5'). I am trying to compare this number with the version number of a different workbook. When I had UserFileBook's 'Version' as an actual named range (it referred to cell C1 which had the value of 5 in it) everything worked fine. But IdiotUser can edit that value or delete it right on the sheet, so I made it just refer to a number so it can only be edited through the manager.
Is there a way for me to obtain the value of that Name and alter it from another WB now? Currently I'm trying this:
Sub CheckVersionNumber(Vers As Long)
'Checks to see if this version is compatible with the UW version
Dim wb As Workbook
Set wb = UserFileBook
Dim UWVers As Long
UWVers = wb.Names("Version").Value 'Breaks here
'Version information is in the range "Version" on UW
If UWVers < Vers Then
GoTo LowerVersion
Else
If wb.Names("Version") > Vers Then 'tried this originally and also breaks, also if .Value is added
GoTo UpperVersion
End If
End If
Exit Sub
I also tried comparing to wb.Range("Version"), and even wb.Worksheets("Sheet 1").Range("Version) but those didnt work either. How can I reference (and alter) the value of "Version" in the USerFileBook if it doesn't refer to a range?
You cannot use .Range because Version is not a range. It's a named formula.
But you can evaluate it:
UWVers = wb.Worksheets(1).Evaluate("Version")
To update the named formula with a different value, say 999:
wb.Names.Add "Version", 999
To make the named formula invisible in the Name Manager:
wb.Names.Add "Version", 999, True
As an aside... since you are having difficulties with users changing your solution settings you may wish to explore utilizing CustomXMLParts.Add to store your Version. There is no user interface to CustomXMLParts, but they are stored in the workbook. The only way to access them is through code. A normal user will NEVER see your version number stored this way. In fact most advanced developers would never find it either.
You can use wb.Names("Version").Value but it returns a string >> =999. Therefore you have to omit the equal-sign before assigning to a long value.
If you want to hide the name from the normal user, you can set the visibility of the name - when adding it the first time - to false. Then the name doesn't show up in the name manager.
I would create a function and a sub.
'---> get current version
Public Function getVersion(wb As Workbook, Optional throwError As Boolean = False) As Long
On Error Resume Next 'in case version does not exist function will return 0
'remove =-sign as from returned value to return a long value
getVersion = Replace(wb.Names("Version").Value, "=", vbNullString)
'if useful you could throw an error here
If Err <> 0 And throwError = True Then
Err.Clear: On Error GoTo 0
Err.Raise vbObjectError, , "Version hasn't been set for this workbook"
End If
On Error GoTo 0
End Function
'--->> set version
Public Sub setVersion(wb As Workbook, newVersion As Long)
On Error Resume Next 'in case version doesn't yet exists
wb.Names("Version").Value = newVersion
If Error > 0 Then
Err.Clear: On Error GoTo 0
'Version name does not yet exist --> add as invisible name
wb.Names.Add "Version", "=" & newVersion, Visible:=False
Else
On Error GoTo 0
End If
End Sub
This is how you use them:
Sub testVersionAsNameConstant()
Debug.Print getVersion(ThisWorkbook, False)
'comment this out if you don't want to see the error
Debug.Print getVersion(ThisWorkbook, True)
setVersion ThisWorkbook, 1
Debug.Print getVersion(ThisWorkbook), "should be 1"
setVersion ThisWorkbook, 2
Dim checkValue As Long
checkValue = 1
Debug.Print getVersion(ThisWorkbook) > checkValue, "should be true"
Debug.Print getVersion(ThisWorkbook) = checkValue, "should be false"
End Sub
Related
I keep having an issue with some code in VBA Excel was looking for some help!
I am trying to sort through a list of names with corresponding phone numbers, checking for multiple names under the same phone number. Then post those names to a separate sheet.
So far my code is:
Sub main()
Dim cName As New Collection
For Each celli In Columns(3).Cells
Sheets(2).Activate
On Error GoTo raa
If Not celli.Value = Empty Then
cName.Add Item:=celli.Row, Key:="" & celli.Value
End If
Next celli
On Error Resume Next
raa:
Sheets(3).Activate
Range("a1").Offset(celli.Row - 1, 0).Value = Range("a1").Offset(cName(celli.Value) - 1, 0).Value
Resume Next
End Sub
When I try to run the code it crashes Excel, and does not give any error codes.
Some things I've tried to fix the issue:
Shorted List of Items
Converted phone numbers to string using cstr()
Adjusted Range and offsets
I'm pretty new to all this, I only managed to get this far on the code with help from other posts on this site. Not sure where to go with this since it just crashes and gives me no error to look into. Any ideas are appreciated Thank you!
Updated:
Option Explicit
Dim output As Worksheet
Dim data As Worksheet
Dim hold As Object
Dim celli
Dim nextRow
Sub main()
Set output = Worksheets("phoneFlags")
Set data = Worksheets("filteredData")
Set hold = CreateObject("Scripting.Dictionary")
For Each celli In data.Columns(3).Cells
On Error GoTo raa
If Not IsEmpty(celli.Value) Then
hold.Add Item:=celli.Row, Key:="" & celli.Value
End If
Next celli
On Error Resume Next
raa:
nextRow = output.Range("A" & Rows.Count).End(xlUp).Row + 1
output.Range("A" & nextRow).Value = data.Range("A1").Offset(hold(celli.Value) - 1, 0).Value
'data.Range("B1").Offset(celli.Row - 1, 0).Value = Range("B1").Offset(hold
Resume Next
End Sub
Update2:
Used hold.Exists along with an ElseIf to remove the GoTo's. Also changed it to copy and paste the row to the next sheet.
Sub main()
Set output = Worksheets("phoneFlags")
Set data = Worksheets("filteredData")
Set hold = CreateObject("Scripting.Dictionary")
For Each celli In data.Columns(2).Cells
If Not hold.Exists(CStr(celli.Value)) Then
If Not IsEmpty(celli.Value) Then
hold.Add Item:=celli.Row, Key:="" & celli.Value
Else
End If
ElseIf hold.Exists(CStr(celli.Value)) Then
data.Rows(celli.Row).Copy (Sheets("phoneFlags").Cells(Rows.Count, 1).End(xlUp).Offset(1, 0))
'output.Range("A" & nextRow).Value = data.Range("A1").Offset(hold(celli.Value) - 1, 0).Value
End If
Next celli
End Sub
When developing code, don't try (or be afraid of) errors as they are pointers to help fix the code or the logic. As such, don't use On Error unless it is absolutely indicated in the coding algorithm (*). using On Error when not necessary only hides errors, does not fix them and when coding it is always better to avoid the errors in the first place (good logic).
When adding to the Dictionary, first check to see if the item already exists. The Microsoft documentation notes that trying to add an element that already exists causes an error. An advantage that the Dictionary object has over an ordinary Collection object in VBA is the .exists(value) method, which returns a Boolean.
The short answer to your question, now that I have the context out of the way, is that you can first check (if Not hold.exists(CStr(celli.Value)) Then) and then add if it does not already exist.
(*) As a side note, I was solving an Excel macro issue yesterday which took me most of the day to nut out, but the raising of errors and the use of debugging code helped me make some stable code rather than some buggy but kind-of-working code (which is what I was fixing in the first place). However, the use of error handling can be a short cut in some instances such as:
Function RangeExists(WS as Worksheet, NamedRange as String) As Boolean
Dim tResult as Boolean
Dim tRange as Range
tResult = False ' The default for declaring a Boolean is False, but I like to be explicit
On Error Goto SetResult ' the use of error means not using a loop through all the named ranges in the WS and can be quicker.
Set tRange = WS.Range(NamedRange) ' will error out if the named range does not exist
tResult = True
On Error Goto 0 ' Always good to explicitly limit where error hiding occurs, but not necessary in this example
SetResult:
RangeExists = tResult
End Function
If ws.Cells(sourceSheetStartColumn, sourceSheetEndColumn - 1) = ""
Then GoTo Here '
The error means you are using one of your variable in a manner that does not fit its type. Specifically:
If ws.Cells(sourceSheetStartColumn, sourceSheetEndColumn - 1) = "" Then GoTo Here '
means that:
ws is a Worksheet
sourceSheetStartColumn and sourceSheetEndColumn are numbers
Check your code to ensure these are declared correctly. Here is an example where this should cause no issues:
Sub Foo()
Dim ws As Worksheet
Dim sourceSheetStartColumn, sourceSheetEndColumn As Integer
Set ws = ActiveWorkbook.ActiveSheet
sourceSheetStartColumn = 2
sourceSheetEndColumn = 2
If ws.Cells(sourceSheetStartColumn, sourceSheetEndColumn - 1) = "" Then GoTo Here '
MsgBox ("Checkpoint 1")
Here:
MsgBox ("Checkpoint 2")
End Sub
So, were I to change your source and end columns to a String and assign values of "A" and "B" to them, I would get your Runtime Error 13.
You will get Run-time error 13 if the cell you are trying to access has an error.
e.g. Cell A1 has formula =NA()
in the immediate window,
?cells(1,1)
gives
Error 2042
if you try to compare this value to anything, you will get Type Mismatch
My code is running perfectly until get to "If c = "" Then". At this point the run time error '-2147418113 (8000ffff)': Automation Error is raised. I have put an On Error Resume Next statement to check if everything goes right if I skip this line and it does. I really don't understand what this error means and I wasn't able to find useful information about it. Could someone bring light to this problem? I have tried to change "If c = "" Then" to "If len(c.value)=0 Then" but it raises the same error. All the variables in AddresRawDataFile are defined as public (as range) and they were set to range in a different module, called PublicVariable, which is called by every procedure.
Private Sub CommandButton3_Ok_Click()
Dim MsgAlert As String
Dim MsgBoxAlert As Variant 'Message box for for many checks done below
Dim c As Variant 'Variable used in a for each structure
Dim AddressRawDataFile As Variant 'Array of variables with address in Box2_UPb_Options
'Code to assign values from Box2_UPb_Options to the related variables
AddressRawDataFile = Array(RawHg202Range, RawPb204Range, RawPb206Range, RawPb207Range, RawPb208Range, RawTh232Range, RawU238Range, _
RawHg202Header, RawPb204HeaderRange, RawPb206HeaderRange, RawPb207HeaderRange, RawPb208HeaderRange, RawTh232HeaderRange, _
RawU238HeaderRange)
'All of the above variables must not be = ""
For Each c In AddressRawDataFile
'On Error Resume Next
If c = "" Then
MsgBoxAlert = MsgBox("There are one or more addresses missing in Start-AND-Options sheet. " & _
"Please, check it.", vbOKOnly, "Missing Address")
Load Box2_UPb_Options
Box2_UPb_Options.MultiPage1.Value = 2
Box2_UPb_Options.Show
End If
Next
As the items named RawHg202Range etc are actually Range objects then you should use Is Nothing to check if they are empty:
AddressRawDataFile = Array(RawHg202Range, RawPb204Range, RawPb206Range, RawPb207Range, RawPb208Range, RawTh232Range, RawU238Range, _
RawHg202Header, RawPb204HeaderRange, RawPb206HeaderRange, RawPb207HeaderRange, RawPb208HeaderRange, RawTh232HeaderRange, _
RawU238HeaderRange)
For Each c In AddressRawDataFile
If c Is Nothing Then
I have a list of names that I need to filter a pivot table on. I use a Case Select to make the pivot items that are equal to the names visible and all other not visible. Can I make a one time array or list of these 15 names and call the list/array name within the code instead of using the 15 names in multiple locations? I have to fun a pivot table and sort on these names many times.
This is what I am trying to avoid. It works as is, but I'm trying to save myself headaches in the future with modifications.
Set table = Worksheets("Sheet2").PivotTables("PivotTable2")
With table.PivotFields("Assigned to")
For Each PvI In .PivotItems
Select Case PvI.Name
Case "Antone", "Brad", "Cavan", "Chris", "Daneisha", "Edward", "James", "Jonathan", "Joesph", "Karen", "Shaun", "Steve", "Timothy", "Tracey"
PvI.Visible = True
Case Else
PvI.Visible = False
End Select
Next
End With
Try this REVERSE Select Case
Sub Sample()
Dim sNames As String
sNames = "/Antone/Brad/Cavan/Chris/Daneisha/Edward/James/Jonathan/Joesph/Karen/Shaun/Steve/Timothy/Tracey/"
Set Table = Worksheets("Sheet2").PivotTables("PivotTable2")
With Table.PivotFields("Assigned to")
For Each PvI In .PivotItems
Select Case 0 '<~~ Reverse Select case
Case InStr(1, sNames, "/" & PvI.Name & "/", vbTextCompare)
PvI.Visible = False '<~~ This also reverses
Case Else
PvI.Visible = True
End Select
Next
End With
End Sub
Here is a simple way to see how Reverse Select Case works :)
Sub Sample()
Dim sNames As String, NameToCheck As String
sNames = "/Antone/Brad/Cavan/Chris/Daneisha/Edward/James/Jonathan/Joesph/Karen/Shaun/Steve/Timothy/Tracey/"
NameToCheck = "Antoneeeee"
'NameToCheck = "Daneisha"
Select Case 0
Case InStr(1, sNames, "/" & NameToCheck & "/", vbTextCompare)
MsgBox "Not found"
Case Else
MsgBox " Found"
End Select
End Sub
Sure and it eliminates the Select Case. I tested the code below minus the pivot table aspect. This stores your names in an array. You can keep the array as a global variable or as a range in the workbook (for the latter change the nameArr = ... line). The code (UBound(Filter(nameArr, PvI.Name)) > -1) will check if PvI.Name exists in nameArr by filtering the array for your desired name and checking if the resulting array has anything in it. Then the true/false value returned by the "array contains check" is assigned to PvI.Visible.
Dim nameArr As Variant
nameArr = Array("Antone", "Brad", "Cavan", "Chris", "Daneisha", "Edward", "James", "Jonathan", "Joesph", "Karen", "Shaun", "Steve", "Timothy", "Tracey")
Set table = Worksheets("Sheet2").PivotTables("PivotTable2")
With table.PivotFields("Assigned to")
For Each PvI In .PivotItems
PvI.Visible = (UBound(Filter(nameArr, PvI.Name)) > -1)
Next
End With
Elaborating on my comment, I recommend a table-driven approach.
The first thing to do is put the names of interest in a worksheet, and give that range a name. I called the range "MyNames" in this example.
The second thing to do is add this function to your project:
Function NameIsInList(LookFor As String) As Boolean
On Error GoTo Err_NameIsInList
NameIsInList = WorksheetFunction.Match(LookFor, Range("MyNames"), 0) > 0
On Error GoTo 0
Exit Function
Err_NameIsInList:
' the name was not found, or a bad parameter was supplied
NameIsInList = False
On Error GoTo 0
End Function
Finally, modify your procedure to use the function:
Set table = Worksheets("Sheet2").PivotTables("PivotTable2")
With table.PivotFields("Assigned to")
For Each PvI In .PivotItems
PvI.Visible = NameIsInList(PvI.Name)
Next
End With
The advantages I was shooting for here are:
Table-driven: The list of values can be stored in a worksheet where it's easily accessible and modified. (If you don't want users to see the LOV you can always hide the worksheet.)
Reusable: Let the function do the work of checking the name list. You suggest this is something that needs to be done many times; the function can be called from multiple places.
I have a dynamically defined named range in my excel ss that grabs data out of a table based on a start date and an end date like this
=OFFSET(Time!$A$1,IFERROR(MATCH(Date_Range_Start,AllDates,0)-1,MATCH(Date_Range_Start,AllDates)),1,MATCH(Date_Range_End,AllDates)-IFERROR(MATCH(Date_Range_Start,AllDates,0)-1,MATCH(Date_Range_Start,AllDates)),4)
But if the date range has no data in the table, the range doesn't exists (or something, idk). How can I write code in VBA to test if this range exists or not?
I have tried something like
If Not Range("DateRangeData") Is Nothing Then
but I get "Runtime error 1004, method 'Range' of object '_Global' failed."
Here is a function I knocked up to return whether a named range exists. It might help you out.
Function RangeExists(R As String) As Boolean
Dim Test As Range
On Error Resume Next
Set Test = ActiveSheet.Range(R)
RangeExists = Err.Number = 0
End Function
You can replicate the match in your VBA to count before using the range how many rows you would have, or you can use error handling:
On Error Resume Next
Debug.Print range("DateRangeData").Rows.Count
If Err = 1004 Then
MsgBox "Range Empty"
Exit Sub
Else
MsgBox "Range full"
End If
Err.Clear
On Error GoTo 0
This is another approach. It has the advantage to take the container and the name you want to test. That means you can test either Sheets Names or Workbook Names for example.
Like this:
If NamedRangeExists(ActiveSheet.Names, "Date") Then
...
Else
...
End If
or
If NamedRangeExists(ActiveWorkbook.Names, "Date") Then
...
Else
...
End If
Public Function NamedRangeExists(ByRef Container As Object, item As String) As Boolean
Dim obj As Object
Dim value As Variant
On Error GoTo NamedRangeExistsError:
value = Container(item)
If Not InStr(1, CStr(value), "#REF!") > 0 Then
NamedRangeExists = True
End If
Exit Function
Exit Function
NamedRangeExistsError:
NamedRangeExists = False
End Function
Depending on the application you're doing, it's good to consider using a Dictionary. They're especially useful when you wanna check whether something exists.
Take this example:
Dim dictNames as Scripting.Dictionary
Sub CheckRangeWithDictionary()
Dim nm As Name
'Initially, check whether names dictionary has already been created
If Not dictNames Is Nothing Then
'if so, dictNames is set to nothing
Set dictNames = Nothing
End If
'Set to new dictionary and set compare mode to text
Set dictNames = New Scripting.Dictionary
dictNames.CompareMode = TextCompare
'For each Named Range
For Each nm In ThisWorkbook.Names
'Check if it refers to an existing cell (bad references point to "#REF!" errors)
If Not (Strings.Right(nm.RefersTo, 5) = "#REF!") Then
'Only in that case, create a Dictionary entry
'The key will be the name of the range and the item will be the address, worksheet included
dictNames(nm.Name) = nm.RefersTo
End If
Next
'You now have a dictionary of valid named ranges that can be checked
End Sub
Within your main procedure, all you need to do is do an existence check before using the range
Sub CopyRange_MyRange()
CheckRangeWithDictionary
If dictNames.exists("MyRange") then
Sheets(1).Range("MyRange").Copy
end if
End Sub
While loading the dictionary may look a little longer, it's extremely fast to process and search. It also becomes much simpler to check whether any named range referring to a valid address exists, without using error handlers in this simple application.
Please note that when using names at sheet level rather than workbook level, it is necessary to use more elaborate keys to guarantee uniqueness. From the way the dictionary was created, if a key is repeated, the item value is overwritten. That can be avoided by using the same Exists method as a check in the key creation statement. If you need a good reference on how to use dictionaries, use this one.
Good luck!
This is an old post, but none of the rated answers has a dynamic solution to test if a name exists in a workbook or worksheet. This function below will accomplish that:
Function pg_Any_Name(thename As String) As Boolean
Dim n As Name, t As String
For Each n In ThisWorkbook.Names
t = Mid(n.Name, InStr(1, n.Name, "!", vbTextCompare) + 1, 999)
If UCase(thename) = UCase(t) Then
pg_Any_Name = True
Exit Function
End If
Next n
End Function
Worth noting that this would not have worked for this specific question because OP had a dynamic defined range. This question would have been more accurately titled Test if Name is a Valid Range because the name always existed as a formula, the issue was if it was a valid RANGE. To address this question with a solution that checks both workbook and sheets... this function would work:
Function PG_Range_Name(thename As String) As Boolean
Dim n As Name, t As String
For Each n In ThisWorkbook.Names
t = Mid(n.Name, InStr(1, n.Name, "!", vbTextCompare) + 1, 999)
If UCase(thename) = UCase(t) Then
On Error Resume Next
PG_Range_Name = n.RefersToRange.Columns.Count > 0
Exit Function
End If
Next n
End Function