Optimizing VBA code running time finding username of a range in another worksheet and return value if found - excel

Good day all ,
I am trying to find each cell value in column A of worksheet "OFSHC" in worksheet "User Assessments" and if value found then return "true" in column V of the corresponding cell in worksheet "OFSHC" else return "false.
I have the code below , however; I am working with +90000 rows in worksheet "OFSHC" and +900000 rows in sheet "User Assessments" , which makes the code to run over 6 hours. any idea on optimizing the code to run for a shorter period of time?
Code:
Sub findUsername_OFSHC_User_Assessments()
Worksheets("OFSHC").Activate
Dim FindString As String
Dim Rng As Range
For Each Cell In Range("A2:A35000")
FindString = Cell.Value
If Trim(FindString) <> "" Then
'The 2nd worksheet is assumed to be User Assessments. Change this if it is not the case
With Sheets("User Assessments").Range("D1:D900000")
Set Rng = .Find(What:=FindString, _
After:=.Cells(.Cells.Count), _
LookIn:=xlValues, _
LookAt:=xlWhole, _
SearchOrder:=xlByRows, _
SearchDirection:=xlNext, _
MatchCase:=False)
If Not Rng Is Nothing Then
Application.GoTo Rng, True
'In Sheet 2: This line shifts 5 cells to the right and gets the country value
'Found = ActiveCell.Offset(0, 5).Value
'In Sheet 1: Found value is pasted into the cell 3 cells to the right of the cell containing the Workday usernme
Cell.Offset(0, 22).Value = "True"
Else
Cell.Offset(0, 22).Value = "False"
End If
End With
End If
Next
End Sub

Lookup Data Using Application.Match
Adjust the values in the constants section.
First, test it on a smaller dataset since it'll still take some time (not tested on a large dataset).
Only run the first procedure which will call the remaining two when necessary.
Option Explicit
Sub findUsername_OFSHC_User_Assessments()
' Constants
Const sName As String = "User Assessments"
Const sFirst As String = "D2"
Const dName As String = "OFSHC"
Const lFirst As String = "A2"
Const dFirst As String = "V2"
' Workbook
Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
' Source
Dim sData As Variant: sData = getColumn(wb.Worksheets(sName).Range(sFirst))
If IsEmpty(sData) Then Exit Sub
' Lookup
Dim ldData As Variant: ldData = getColumn(wb.Worksheets(dName).Range(lFirst))
If IsEmpty(ldData) Then Exit Sub
Dim rCount As Long: rCount = UBound(ldData, 1)
' Destination
Dim r As Long
For r = 1 To rCount
If IsNumeric(Application.Match(ldData(r, 1), sData, 0)) Then
ldData(r, 1) = True ' "'True"
Else
ldData(r, 1) = False ' "'False"
End If
Next r
' Write
writeDataSimple wb.Worksheets(dName).Range(dFirst), ldData, True
End Sub
Function getColumn( _
FirstCellRange As Range) _
As Variant
Const ProcName As String = "getColumn"
On Error GoTo clearError
If Not FirstCellRange Is Nothing Then
With FirstCellRange.Cells(1)
Dim lCell As Range
Set lCell = .Resize(.Worksheet.Rows.Count - .Row + 1) _
.Find("*", , xlFormulas, , , xlPrevious)
If Not lCell Is Nothing Then
Dim rCount As Long: rCount = lCell.Row - .Row + 1
Dim Data As Variant
If rCount = 1 Then
ReDim Data(1 To 1, 1 To 1): Data(1, 1) = .Value
Else
Data = .Resize(rCount).Value
End If
getColumn = Data
End If
End With
End If
ProcExit:
Exit Function
clearError:
Debug.Print "'" & ProcName & "': Unexpected Error!" & vbLf _
& " " & "Run-time error '" & Err.Number & "':" & vbLf _
& " " & Err.Description
Resume ProcExit
End Function
Sub writeDataSimple( _
ByVal FirstCellRange As Range, _
ByVal Data As Variant, _
Optional ByVal doClearContents As Boolean = True)
Const ProcName As String = "writeDataSimple"
On Error GoTo clearError
If Not FirstCellRange Is Nothing Then
If Not IsEmpty(Data) Then
Dim rCount As Long: rCount = UBound(Data, 1)
With FirstCellRange.Cells(1).Resize(, UBound(Data, 2))
.Resize(rCount).Value = Data
If doClearContents Then
.Resize(.Worksheet.Rows.Count - .Row - rCount + 1) _
.Offset(rCount).ClearContents
End If
End With
End If
End If
ProcExit:
Exit Sub
clearError:
Debug.Print "'" & ProcName & "': Unexpected Error!" & vbLf _
& " " & "Run-time error '" & Err.Number & "':" & vbLf _
& " " & Err.Description
Resume ProcExit
End Sub

Here's a dictionary-based example using the same range sizes (35k lookup values against a 900k list).
In my testing it ran in < 10sec.
Notes:
Loading up a dictionary gets progressively slower as the number of items get larger, so here we're keeping the size below 100k by using a bunch of dictionaries, which collectively all load faster (~8-9sec) than loading all the values into a single dictionary (>50sec). We lose a bit of speed on the lookups, but still much faster.
This is based on all of your ColA values being unique - if they're not then whether or not that matters would depend on your exact use case. In this specific instance you're just looking for any match, so it's OK, but if for example you wanted to find all matches from a non-unique list you'd need to re-work the approach.
Sub Tester()
Dim dict, arr, t, r As Long, arr2, arrRes, i As Long
Dim colDicts As New Collection, arrK, res As Boolean
t = Timer
Set dict = CreateObject("scripting.dictionary")
arr = Range("A2:A900000").Value 'the lookup range
For r = 1 To UBound(arr, 1)
If r Mod 100000 = 1 Then
Set dict = CreateObject("scripting.dictionary")
colDicts.Add dict
End If
dict(arr(r, 1)) = True
Next r
Debug.Print "Loaded dictionaries", Timer - t
arr2 = Range("C2:C35000").Value 'values to be found
ReDim arrRes(1 To UBound(arr2, 1), 1 To 1) 'size array for results
For r = 1 To UBound(arr2, 1)
res = False
For Each dict In colDicts 'check each dictionary
If dict.exists(arr2(r, 1)) Then
res = True
Exit For 'done checking
End If
Next dict
arrRes(r, 1) = res 'assign true/false
Next r
Range("D2").Resize(UBound(arr2, 1), 1).Value = arrRes
Debug.Print "Done", Timer - t '< 10sec
End Sub

Related

VBA: Keep first and last rows of duplicate column values of an Excel sheet

I have an Excel worksheet with 20K rows like this:
header1
header2
1
P
2
P
3
P
4
Q
5
R
6
R
7
R
8
R
9
S
10
S
I want a VBA code to delete the rows containing duplicates, but keep the first and last row of the duplicates. The result should be like this:
header1
header2
1
P
3
P
4
Q
5
R
8
R
9
S
10
S
I have modified the following code found here to do just that, but every time I have to manually select the range containing the duplicates in column header2.
Sub Delete_Dups_Keep_Last_v2()
Dim SelRng As Range
Dim Cell_in_Rng As Range
Dim RngToDelete As Range
Dim SelLastRow As Long
Application.DisplayAlerts = False
Set SelRng = Application.InputBox("Select cells", Type:=8)
On Error GoTo 0
Application.DisplayAlerts = True
SelLastRow = SelRng.Rows.Count + SelRng.Row - 1
For Each Cell_in_Rng In SelRng
If Cell_in_Rng.Row < SelLastRow Then
If Cell_in_Rng.Row > SelRng.Row Then
If Not Cell_in_Rng.Offset(1, 0).Resize(SelLastRow - Cell_in_Rng.Row).Find(What:=Cell_in_Rng.Value, Lookat:=xlWhole) Is Nothing Then
'this value exists again in the range
If RngToDelete Is Nothing Then
Set RngToDelete = Cell_in_Rng
Else
Set RngToDelete = Application.Union(RngToDelete, Cell_in_Rng)
End If
End If
End If
End If
Next Cell_in_Rng
If Not RngToDelete Is Nothing Then RngToDelete.EntireRow.Delete
End Sub
Another code found here by user A.S.H. automates the manual selection and speed by using Dictionary, but fails to produce the wanted result.
Sub keepFirstAndLast()
Dim toDelete As Range: Set toDelete = Sheet1.Rows(999999) '(to not start with a null range)
Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
Dim a As Range
For Each a In Sheet1.Range("B2", Sheet1.Range("B999999").End(xlUp))
If Not dict.Exists(a.Value2) Then
dict(a.Value2) = 0 ' first appearence, dont save the row
Else
' if last observed occurrence was a duplicate, add it to deleted range
If dict(a.Value2) > 0 Then Set toDelete = Union(toDelete, Sheet1.Rows(dict(a.Value2)))
dict(a.Value2) = a.row ' not first appearence, save the row for eventual deletion
End If
Next
toDelete.Delete
End Sub
Simple solution:
Sub KeepFirstLast()
Application.ScreenUpdating = False
Dim lastRow As Long
lastRow = Sheets(1).Cells(Rows.Count, 1).End(xlUp).Row
Dim i As Long
Dim x As Long
Dim currentValue As String
For i = lastRow To 2 Step -1
If i = 2 Then
Application.ScreenUpdating = True
Exit For
End If
currentValue = Sheets(1).Cells(i, 2).Value
x = i - 1
Do While Sheets(1).Cells(x, 2).Value = currentValue And Sheets(1).Cells(x - 1, 2).Value = currentValue
Sheets(1).Rows(x).Delete
x = x - 1
Loop
i = x + 1
Next i
Application.ScreenUpdating = True
End Sub
You may benefit from SpecialCells to select those rows based on formula:
Sub test()
Dim LR As Long 'last row
Dim LC As Long 'last column
Dim SR As Long 'starting row
Dim rng As Range
Set rng = Range("A1") 'change this to TOP LEFT CELL OF YOUR DATA
SR = rng.Row
LR = rng.CurrentRegion.Cells(rng.CurrentRegion.Rows.Count, 1).Row
LC = Cells(1, Columns.Count).End(xlToLeft).Column 'last column used
'we add new column with formula to delete
With Range(Cells(SR + 1, LC + 1), Cells(LR, LC + 1))
.FormulaR1C1 = "=IF(OR(RC[-1]<>R[-1]C[-1],RC[-1]<>R[1]C[-1]),""x"",0)"
.SpecialCells(xlCellTypeFormulas, 1).EntireRow.Delete
End With
'clear formula
LR = rng.CurrentRegion.Cells(rng.CurrentRegion.Rows.Count, 1).Row
Range(Cells(SR + 1, LC + 1), Cells(LR, LC + 1)).Clear
Set rng = Nothing
End Sub
[![enter image description here][1]][1]
The tricky part is here:
.FormulaR1C1 = "=IF(OR(RC[-1]<>R[-1]C[-1],RC[-1]<>R[1]C[-1]),""x"",0)"
.SpecialCells(xlCellTypeFormulas, 1).EntireRow.Delete
First line will create and IF(OR) formula to check if the row must be deleted or not. It will return x if not, else 0
Second line will delete entire rows only if it contains a number (zero)
[1]: https://i.stack.imgur.com/UlhtI.gif
This can also be accomplished using Power Query, available in Windows Excel 2010+ and Excel 365 (Windows or Mac)
To use Power Query
Select some cell in your Data Table
Data => Get&Transform => from Table/Range or from within sheet
When the PQ Editor opens: Home => Advanced Editor
Make note of the Table Name in Line 2
Paste the M Code below in place of what you see
Change the Table name in line 2 back to what was generated originally.
Read the comments and explore the Applied Steps to understand the algorithm
M Code
let
//change next line to your actual table name in your worksheet
Source = Excel.CurrentWorkbook(){[Name="Table1"]}[Content],
#"Changed Type" = Table.TransformColumnTypes(Source,{{"header1", Int64.Type}, {"header2", type text}}),
//Group by header2
// then return the first and last table rows if there is more than a single row
#"Grouped Rows" = Table.Group(#"Changed Type", {"header2"}, {
{"header1", each if Table.RowCount(_) = 1 then _
else Table.FromRecords({Table.First(_),Table.Last(_)}),
type table[header1=Int64.Type, header2=text]}
}),
//expand the subtables and set the column order
#"Expanded header1" = Table.ExpandTableColumn(#"Grouped Rows", "header1", {"header1"}),
#"Reordered Columns" = Table.ReorderColumns(#"Expanded header1",{"header1", "header2"})
in
#"Reordered Columns"
Keep First and Last In Sorted Range
Option Explicit
Sub DeleteNotFirstNorLast()
Const ProcName As String = "DeleteNotFirstNorLast"
Dim RowsDeleted As Boolean ' to inform
On Error GoTo ClearError ' enable error trapping
' Constants (adjust!)
Const FirstCellAddress As String = "A1"
Const CriteriaColumnIndex As Long = 2
Const Criteria As String = "#$%"
' Reference the worksheet.
Dim ws As Worksheet: Set ws = ActiveSheet ' improve!
Application.ScreenUpdating = False
' Turn off AutoFilter.
If ws.AutoFilterMode Then ws.AutoFilterMode = False
' Reference the table range.
Dim trg As Range: Set trg = RefCurrentRegion(ws.Range(FirstCellAddress))
' Write an ascending integer sequence adjacent to the right
' of the table range.
AppendColumnOfAscendingIntegers trg
' Include this helper column to the table range.
Set trg = trg.Resize(, trg.Columns.Count + 1)
' Reference the criteria column range.
Dim crg As Range: Set crg = trg.Columns(CriteriaColumnIndex)
' It is assumed that the criteria column is already sorted favorably.
' If not, you could do something like the following:
' Sort the table range by the criteria column ascending.
'trg.Sort crg, xlAscending, , , , , , xlYes
' Write the data rows (no headers) count to a variable.
Dim drCount As Long: drCount = trg.Rows.Count - 1
' Reference the criteria column data range (headers excluded).
Dim cdrg As Range: Set cdrg = crg.Resize(drCount).Offset(1)
' Write the values from the criteria column data range to an array.
Dim cData As Variant: cData = GetRange(cdrg)
' Replace the unwanted values in the array with the criteria.
KeepFirstAndLastInColumn cData
' Write the (modified) values from the array back to the range.
cdrg.Value = cData
' Reference the table data range (no headers).
Dim tdrg As Range: Set tdrg = trg.Resize(drCount).Offset(1)
' Filter the table range in the criteria column by the criteria.
trg.AutoFilter CriteriaColumnIndex, Criteria
' Attempt to reference the table data visible (filtered) range.
Dim tdvrg As Range
On Error Resume Next ' defer error trapping
Set tdvrg = tdrg.SpecialCells(xlCellTypeVisible)
On Error GoTo ClearError ' re-enable error trapping
' Remove the filter.
ws.AutoFilterMode = False
' Attempt to delete the table data visible range.
If Not tdvrg Is Nothing Then
tdvrg.Delete xlShiftUp
RowsDeleted = True
End If
' Reference the helper column.
Dim hrg As Range: Set hrg = trg.Columns(trg.Columns.Count)
' Sort the table range by the helper column ascending.
trg.Sort hrg, xlAscending, , , , , , xlYes
' Clear the helper column.
hrg.Clear
SafeExit:
Application.ScreenUpdating = True ' to see any changes while reading message
' Inform.
If RowsDeleted Then
MsgBox "Rows deleted.", vbInformation, ProcName
Else
MsgBox "Nothing deleted.", vbExclamation, ProcName
End If
Exit Sub
ClearError:
Debug.Print "'" & ProcName & "' Run-time error '" _
& Err.Number & "':" & vbLf & " " & Err.Description
Resume SafeExit
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Returns a reference to the range starting with the first cell
' of a range and ending with the last cell of the first cell's
' Current Region.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function RefCurrentRegion( _
ByVal FirstCell As Range) _
As Range
Const ProcName As String = "RefCurrentRegion"
On Error GoTo ClearError
If FirstCell Is Nothing Then Exit Function
With FirstCell.Cells(1).CurrentRegion
Set RefCurrentRegion = FirstCell.Resize(.Row + .Rows.Count _
- FirstCell.Row, .Column + .Columns.Count - FirstCell.Column)
End With
ProcExit:
Exit Function
ClearError:
Debug.Print "'" & ProcName & "' Run-time error '" _
& Err.Number & "':" & vbLf & " " & Err.Description
Resume ProcExit
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Writes an ascending integer sequence adjacent to the right
' of a range.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Sub AppendColumnOfAscendingIntegers( _
ByVal trg As Range, _
Optional ByVal FirstInteger As Long = 1)
Const ProcName As String = "AppendColumnOfAscendingIntegers"
On Error GoTo ClearError
With trg
With .Resize(, 1).Offset(, .Columns.Count)
.Value = .Worksheet.Evaluate("ROW(" & CStr(FirstInteger) & ":" _
& CStr(FirstInteger + .Rows.Count - 1) & ")")
End With
End With
ProcExit:
Exit Sub
ClearError:
Debug.Print "'" & ProcName & "' Run-time error '" _
& Err.Number & "':" & vbLf & " " & Err.Description
Resume ProcExit
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Returns the values of a range ('trg') in a 2D one-based array.
' Remarks: If ˙rg` refers to a multi-range, only its first area
' is considered.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function GetRange( _
ByVal trg As Range) _
As Variant
Const ProcName As String = "GetRange"
On Error GoTo ClearError
If trg.Rows.Count + trg.Columns.Count = 2 Then ' one cell
Dim Data As Variant: ReDim Data(1 To 1, 1 To 1): Data(1, 1) = trg.Value
GetRange = Data
Else ' multiple cells
GetRange = trg.Value
End If
ProcExit:
Exit Function
ClearError:
Debug.Print "'" & ProcName & "' Run-time error '" _
& Err.Number & "':" & vbLf & " " & Err.Description
Resume ProcExit
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: In the first column of a 2D one-based array of sorted values,
' keeps the first and last occurrence of each value and replaces
' the remaining occurrences with a string.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Sub KeepFirstAndLastInColumn( _
ByRef cData As Variant, _
Optional ByVal Criteria As String = "#$%")
Const ProcName As String = "KeepFirstAndLastInColumn"
On Error GoTo ClearError
Dim OldString As String: OldString = CStr(cData(1, 1))
Dim r As Long
Dim cr As Long
Dim FirstRow As Long
Dim NewString As String
For r = 2 To UBound(cData, 1)
NewString = CStr(cData(r, 1))
If NewString = OldString Then
If FirstRow = 0 Then
FirstRow = r
End If
Else
If FirstRow > 0 Then
For cr = FirstRow To r - 2
cData(cr, 1) = Criteria
Next cr
FirstRow = 0
End If
OldString = NewString
End If
Next r
ProcExit:
Exit Sub
ClearError:
Debug.Print "'" & ProcName & "' Run-time error '" _
& Err.Number & "':" & vbLf & " " & Err.Description
Resume ProcExit
End Sub

How to correct code so that it runs or inserts formula in column to left of Range for cells that = "Metered"

I have a worksheet where column C has a formula that looks up value if column D = "Metered".
Users, who are mostly farm workers, have the ability to overwrite it (or possibly delete it using the Make Correction button). Unless column D = "Metered", I don't care if column C is overwritten because data validation makes sure entry is OK. Users are supposed to Tab past column C unless load is "Metered". As a failsafe, I duplicated the "Metered" lookup formula elsewhere and the results are in column S. I don't get any errors on code below, but it doesn't do anything -- previous versions would do things but not the right things. Clearly, I cannot solve this on my own and very much appreciate any help you can provide. I want to run the failsafe once a day when the workbook is opened (running on laptops and speed is important).
Private Sub Workbook_Open()
Application.OnTime TimeValue("02:57:00"), "SaveBeforeDailyRestart"
Application.MoveAfterReturnDirection = xlToRight
Call MeteredLookupRefreshFormula
End Sub
Sub MeteredLookupRefreshFormula()
Sheet1.Unprotect Password:="Cami8"
Dim bng As Range
Set bng = Range("D8:D10009")
For Each cell In bng
If Value = "Metered" Then
bng.Offset(0, -1).Select
Selection.Value = "S & ActiveCell.Row)"
Else
End If
Next
Sheet1.Protect Password:="Cami8"
End Sub
Loop Through Cells
A Quick Fix (Slow)
To not be dependent on the offset you could additionally do:
cell.EntireRow.Columns("C").Value = cell.EntireRow.Columns("S").Value
Sub MeteredLookupRefreshFormulaQuickFix()
With Sheet1
.Unprotect Password:="Cami8"
With .Range("D8:D10009")
Dim cell As Range
For Each cell In .Cells
If StrComp(CStr(cell.Value), "Metered", vbTextCompare) = 0 Then
cell.Offset(0, -1).Value = cell.EntireRow.Columns("S").Value
End If
Next cell
End With
.Protect Password:="Cami8"
End With
End Sub
An Improvement (Fast)
If you have many cells containing formulas evaluating to an empty string ="" at the bottom of column D, replace xlFormulas with xlValues for these cells not to be processed and speed up even more.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Refreshes...
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Calls: RefColumn,GetRange.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Sub MeteredLookupRefreshFormula()
Const cfcAddress As String = "D8"
Const dCol As String = "C"
Const sCol As String = "S"
Const Criteria As String = "Metered"
Const pw As String = "Cami8"
Sheet1.Unprotect Password:=pw
Dim crg As Range: Set crg = RefColumn(Sheet1.Range(cfcAddress))
If crg Is Nothing Then Exit Sub ' no data
Dim cData As Variant: cData = GetRange(crg)
Dim drg As Range: Set drg = crg.EntireRow.Columns(dCol)
Dim dData As Variant: dData = GetRange(drg)
Dim sData As Variant: sData = GetRange(crg.EntireRow.Columns(sCol))
Dim r As Long
For r = 1 To UBound(cData, 1)
If StrComp(CStr(cData(r, 1)), Criteria, vbTextCompare) = 0 Then
dData(r, 1) = sData(r, 1)
End If
Next r
drg.Value = dData
Sheet1.Protect Password:=pw
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Creates a reference to the one-column range ('crg') whose first
' cell is defined by the first cell of the range ('FirstCell')
' and whose last cell is the bottom-most non-empty cell
' of the first cell's worksheet column.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function RefColumn( _
ByVal FirstCell As Range) _
As Range
Const ProcName As String = "RefColumn"
On Error GoTo ClearError
With FirstCell.Cells(1)
Dim lCell As Range
Set lCell = .Resize(.Worksheet.Rows.Count - .Row + 1) _
.Find("*", , xlFormulas, , , xlPrevious)
If lCell Is Nothing Then Exit Function
Set RefColumn = .Resize(lCell.Row - .Row + 1)
End With
ProcExit:
Exit Function
ClearError:
Debug.Print "'" & ProcName & "' Run-time error '" _
& Err.Number & "':" & vbLf & " " & Err.Description
Resume ProcExit
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Returns the values of a range ('rg') in a 2D one-based array.
' Remarks: If ˙rg` refers to a multi-range, only its first area
' is considered.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function GetRange( _
ByVal rg As Range) _
As Variant
Const ProcName As String = "GetRange"
On Error GoTo ClearError
If rg.Rows.Count + rg.Columns.Count = 2 Then ' one cell
Dim Data As Variant: ReDim Data(1 To 1, 1 To 1): Data(1, 1) = rg.Value
GetRange = Data
Else ' multiple cells
GetRange = rg.Value
End If
ProcExit:
Exit Function
ClearError:
Debug.Print "'" & ProcName & "' Run-time error '" _
& Err.Number & "':" & vbLf & " " & Err.Description
Resume ProcExit
End Function
It appears as though the contents of your FOR loop is all screwed up. This is untested but change this ...
For Each cell In bng
If Value = "Metered" Then
bng.Offset(0, -1).Select
Selection.Value = "S & ActiveCell.Row)"
Else
End If
Next
... to this ...
For Each cell In bng
If cell.Value = "Metered" Then
cell.Offset(0, -1).Value = cell.Worksheet.Range("S" & cell.Row).Value
End If
Next
... and it should help.

VBA filtering list using another list

I'm trying to filter the ID in a table, using IDs from another list. However, when I try to do so the macro only filters the first value in the list.
Table Format
Code:
Sub Test()
Dim wb As Workbook
Set wb = ThisWorkbook
ActiveSheet.AutoFilterMode = False
Workbooks.Open "C:\List.xlsx"
Criteria = Worksheets("DataArray").Range("A3:A103")
wb.Activate
ActiveSheet.Range("$A$8:$BE$5000").AutoFilter Field:=3, Criteria1:=Criteria, Operator:=xlFilterValues
End Sub
The "List" is in another workbook so I need the macro to open it first.
When I try changing the Range to A4:A103 the filter will just use the A4 (first value in the range).
Try the next way, please:
Dim Crit As Variant
Set Crit = Worksheets("DataArray").Range("A3:A103").Value
ActiveSheet.Range("$A$8:$BE$5000").AutoFilter Field:=3, Criteria1:=Application.Transpose(Crit), Operator:=xlFilterValues
The column list must be transposed on a row. Otherwise, only its first element will be used.
You can do all this by selecting the ranges (directly in the Excel interface). The following code is reusable:
Option Explicit
Public Sub FilterBySelection()
Dim rngFirst As Range
Dim rngSecond As Range
'
'Get Ranges from User Selection
Set rngFirst = GetRangeBySelection("Select range to filter!" _
& vbNewLine & "Please select a single continuous range!" _
& vbNewLine & vbNewLine & "Note that by selecting a single cell, your" _
& " selection will default to the current region for that cell!" _
, "Select Range")
If rngFirst Is Nothing Then Exit Sub
'
Set rngSecond = GetRangeBySelection("Select range containing filtering values!" _
& vbNewLine & "Please select a single continuous range!" _
& vbNewLine & vbNewLine & "Note that by selecting a single cell, your" _
& " selection will default to the current region for that cell!" _
, "Select Range")
If rngSecond Is Nothing Then Exit Sub
'
'Filter first range using values from the second range
Dim arrValues() As Variant: arrValues = rngSecond.Value2
Dim arrCriteria() As Variant
Dim i As Long
Dim v As Variant
'
'Criteria values must be a 1-dimension array
ReDim arrCriteria(0 To rngSecond.Count - 1)
i = 0
For Each v In arrValues
arrCriteria(i) = CStr(v) 'Criteria must be string data type
i = i + 1
Next v
'
'Filter
On Error Resume Next
If rngFirst.ListObject Is Nothing Then
rngFirst.AutoFilter
rngFirst.AutoFilter Field:=1, Criteria1:=arrCriteria, Operator:=xlFilterValues
Else
With rngFirst.ListObject.Range
.AutoFilter Field:=rngFirst.Column - .Column + 1 _
, Criteria1:=arrCriteria, Operator:=xlFilterValues
End With
End If
On Error GoTo 0
End Sub
Public Function GetRangeBySelection(ByVal prompt_ As String, ByVal title_ As String) As Range
Dim rng As Range
'
Do While rng Is Nothing
On Error Resume Next
Set rng = Application.InputBox(Prompt:=prompt_, Title:=title_, Type:=8)
If rng.Cells.Count = 1 Then Set rng = rng.CurrentRegion
On Error GoTo 0
If rng Is Nothing Then Exit Function
'
On Error GoTo ErrorHandler
If rng.Areas.Count > 1 Then
If MsgBox("Your selection contains " & rng.Areas.Count _
& " different ranges!" & vbNewLine & "Please select only 1 " _
& "range!", vbQuestion + vbRetryCancel, "Cancelled") _
<> vbRetry Then Exit Function
Set rng = Nothing
ElseIf rng.Cells.Count = 1 Then
If MsgBox("No region found from selected cell" & vbNewLine _
& "Please select more than 1 cell!", vbQuestion _
+ vbRetryCancel, "Cancelled") <> vbRetry Then Exit Function
Set rng = Nothing
ElseIf rng.Rows.Count = 1 Then
If MsgBox("Please select more than 1 row!", vbQuestion _
+ vbRetryCancel, "Cancelled") <> vbRetry Then Exit Function
Set rng = Nothing
End If
Loop
Set GetRangeBySelection = rng
Exit Function
ErrorHandler:
MsgBox "Try selecting a smaller range next time", vbInformation, "Cancelled"
End Function
Just run the FilterBySelection method
EDIT 1
Or, if you would like to have less restrictions and be able to select multiple ranges for the filtering values then use this instead:
Option Explicit
Public Sub FilterBySelection()
Dim rngFirst As Range
Dim rngSecond As Range
'
'Get Ranges from User Selection
Set rngFirst = GetRangeBySelection("Select range to filter!" _
& vbNewLine & "Please select a single continuous range!" _
, "Select Range", False)
If rngFirst Is Nothing Then Exit Sub
'
Set rngSecond = GetRangeBySelection("Select range(s) containing filtering values!" _
, "Select Range", True)
If rngSecond Is Nothing Then Exit Sub
'
'Filter first range using values from the second range
Dim rng As Range
Dim arrValues() As Variant
Dim arrCriteria() As Variant
Dim i As Long
Dim v As Variant
'
'Criteria values must be a 1-dimension array
i = 0
ReDim arrCriteria(0 To rngSecond.Count - 1)
For Each rng In rngSecond.Areas
If rng.Count = 1 Then
ReDim arrValues(0 To 0)
arrValues(0) = rng.Value2
Else
arrValues = rng.Value2
End If
For Each v In arrValues
arrCriteria(i) = CStr(v) 'Criteria must be string data type
i = i + 1
Next v
Next
'
'Filter
On Error Resume Next
If rngFirst.ListObject Is Nothing Then
rngFirst.AutoFilter
rngFirst.AutoFilter Field:=1, Criteria1:=arrCriteria, Operator:=xlFilterValues
Else
With rngFirst.ListObject.Range
.AutoFilter Field:=rngFirst.Column - .Column + 1 _
, Criteria1:=arrCriteria, Operator:=xlFilterValues
End With
End If
On Error GoTo 0
End Sub
Public Function GetRangeBySelection(ByVal prompt_ As String, ByVal title_ As String _
, allowMultiArea As Boolean) As Range
Dim rng As Range
'
Do While rng Is Nothing
On Error Resume Next
Set rng = Application.InputBox(Prompt:=prompt_, Title:=title_, Type:=8)
On Error GoTo 0
If rng Is Nothing Then Exit Function
'
On Error GoTo ErrorHandler
If rng.Areas.Count > 1 And Not allowMultiArea Then
If MsgBox("Your selection contains " & rng.Areas.Count _
& " different ranges!" & vbNewLine & "Please select only 1 " _
& "range!", vbQuestion + vbRetryCancel, "Cancelled") _
<> vbRetry Then Exit Function
Set rng = Nothing
End If
Loop
Set GetRangeBySelection = rng
Exit Function
ErrorHandler:
MsgBox "Try selecting a smaller range next time", vbInformation, "Cancelled"
End Function

How can I tell where Named Ranges are acutally used? [duplicate]

I have a list of 594 named ranges in a workbook with nearly 20 sheets, each sheet has about 200 columns of data. I need to find out where the named ranges are being used so as to remove irrelevant ones. I pasted a list of named ranges onto the sheet and then I tried to find if they were used in a formula by recording them, and then using the find method in all sheets and columns. The problem is despite using lookin xlformulas, it retrieves the named range even if it is just a text.
Here is my (updated) attempt (if it is not evident already, i am an amateur):
Application.ScreenUpdating = False
Count = ActiveWorkbook.Sheets.Count
Sheets(Count).Activate
Dim locr(1 To 595)
Dim locc(1 To 595)
Dim locn(1 To 595)
Dim nam(1 To 595)
Dim rng As Range
Range("a1").Select
For X = 1 To 595 'populate array with named ranges
ActiveCell.Offset(1, 0).Select
nam(X) = ActiveCell.Value
Next X
For i = 1 To 595 'name loop
For j = 1 To (Count - 1) 'sheet loop
Sheets(j).Activate
On Error Resume Next
Set orange = Sheets(j).Cells.SpecialCells(xlCellTypeFormulas) 'limit range to cells that only contain formulas
On Error GoTo 20 'if no formulas in sheet, go to next sheet
If Not orange Is Nothing Then
Set rng = orange.Find(What:=nam(i), _
LookIn:=xlFormulas, _
LookAt:=xlPart, _
SearchOrder:=xlByRows, _
SearchDirection:=xlNext, _
MatchCase:=False) 'find named range
If Not rng Is Nothing Then 'if named range found
Application.Goto rng, True 'go to cell where name range found and record address
locr(i) = ActiveCell.Row
locc(i) = ActiveCell.Column
locn(i) = ActiveSheet.Name
GoTo 10 'value found, go to next sheet
Else
End If
Else
End If
20 Next j
locr(i) = "" 'record empty since "rng" is empty
locr(i) = ""
locr(i) = ""
10 Next i
Sheets(Count).Activate
Range("c1").Select
b = 1
For a = 1 To 595 'populate addresses of named ranges
ActiveCell.Offset(b, 2).Value = locr(a)
ActiveCell.Offset(b, 1).Value = locc(a)
ActiveCell.Offset(b, 0).Value = locn(a)
b = b + 1
Next a
Here is one way I can think of. I will explain this in 2 parts.
PART 1
Let's say we have a named range Sid.
This word Sid can appear in any one of these forms as shown in the image below. Why does it start with =? That has been explained in Part2 below.
=Sid '<~~ 1
="Sid" '<~~ 2
=XSid '<~~ 3
=SidX '<~~ 4
=_Sid '<~~ 5
=Sid_ '<~~ 6
=(Sid) '<~~ 7
Any other scenarios, I guess will be a subset of the above. Now out of these the only valid find in our case is the first one and the last one since we are looking for our named range.
So here is a quick function to check if the cell formula has a named range or not. I am sure it can be made more efficient
Function isNamedRangePresent(rng As Range, s As String) As Boolean
Dim sFormula As String
Dim pos1 As Long, pos2 As Long, sLen As Long, i As Long
sFormula = rng.Formula: sLen = Len(sFormula)
pos2 = 1
Do
pos1 = InStr(pos2, sFormula, s) - 1
If pos1 < 1 Then Exit Do
isNamedRangePresent = True
For i = 65 To 90
'~~> A-Z before Sid for example XSid
If UCase(Mid(sFormula, pos1, 1)) = Chr(i) Then
isNamedRangePresent = False
Exit For
End If
Next i
'~~> Check for " for example "Sid
If isNamedRangePresent = True Then _
If UCase(Mid(sFormula, pos1, 1)) = Chr(34) Then isNamedRangePresent = False
'~~> Check for underscore for example _Sid
If isNamedRangePresent = True Then _
If UCase(Mid(sFormula, pos1, 1)) = Chr(95) Then isNamedRangePresent = False
pos2 = pos1 + Len(s) + 1
If pos2 <= sLen Then
For i = 65 To 90
'~~> A-Z after Sid for example SidX
If UCase(Mid(sFormula, pos2, 1)) = Chr(i) Then
isNamedRangePresent = False
Exit For
End If
Next i
'~~> "Sid
If isNamedRangePresent = True Then _
If UCase(Mid(sFormula, pos2, 1)) = Chr(34) Then isNamedRangePresent = False
'~~> _Sid
If isNamedRangePresent = True Then _
If UCase(Mid(sFormula, pos2, 1)) = Chr(95) Then isNamedRangePresent = False
End If
Loop
End Function
So in the first and the last case, Debug.Print isNamedRangePresent(Range("D2"), "Sid") will give you True See this
PART 2
Now coming to the .Find. I see that you are searching only once in the worksheet. Since you can have many scenarios of the word Sid being present, you cannot just have one .Find. You will have to use .FindNext. See THIS link on how to use that. I have explained it there so I won't bother explaining that here.
We can make our .Find more efficient by searching only those cells which has formulas. To do that we have to use .SpecialCells(xlCellTypeFormulas). This explains why we had "=" in our example in PART1. :)
Here is an example (PART1 Code added at the bottom)
Sub Sample()
Dim oRange As Range, aCell As Range, bCell As Range
Dim oSht As Worksheet
Dim strSearch As String, FoundAt As String
Set oSht = Worksheets("Sheet1")
'~~> Set your range where you need to find - Only Formula Cells
On Error Resume Next
Set oRange = oSht.Cells.SpecialCells(xlCellTypeFormulas)
On Error GoTo 0
If Not oRange Is Nothing Then
strSearch = "Sid"
Set aCell = oRange.Find(What:=strSearch, LookIn:=xlFormulas, _
LookAt:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, _
MatchCase:=False, SearchFormat:=False)
If Not aCell Is Nothing Then
Set bCell = aCell
'~~> Check if the cell has named range
If isNamedRangePresent(aCell, strSearch) Then FoundAt = aCell.Address
Do
Set aCell = oRange.FindNext(After:=aCell)
If Not aCell Is Nothing Then
If aCell.Address = bCell.Address Then Exit Do
'~~> Check if the cell has named range
If isNamedRangePresent(aCell, strSearch) Then FoundAt = FoundAt & ", " & aCell.Address
Else
Exit Do
End If
Loop
Else
MsgBox SearchString & " not Found"
Exit Sub
End If
If FoundAt = "" Then
MsgBox "The Named Range was not found"
Else
MsgBox "The Named Range has been found these locations: " & FoundAt
End If
End If
End Sub
Function isNamedRangePresent(rng As Range, s As String) As Boolean
Dim sFormula As String
Dim pos1 As Long, pos2 As Long, sLen As Long, i As Long
sFormula = rng.Formula: sLen = Len(sFormula)
pos2 = 1
Do
pos1 = InStr(pos2, sFormula, s) - 1
If pos1 < 1 Then Exit Do
isNamedRangePresent = True
For i = 65 To 90
'~~> A-Z before Sid for example XSid
If UCase(Mid(sFormula, pos1, 1)) = Chr(i) Then
isNamedRangePresent = False
Exit For
End If
Next i
'~~> Check for " for example "Sid
If isNamedRangePresent = True Then _
If UCase(Mid(sFormula, pos1, 1)) = Chr(34) Then isNamedRangePresent = False
'~~> Check for underscore for example _Sid
If isNamedRangePresent = True Then _
If UCase(Mid(sFormula, pos1, 1)) = Chr(95) Then isNamedRangePresent = False
pos2 = pos1 + Len(s) + 1
If pos2 <= sLen Then
For i = 65 To 90
'~~> A-Z after Sid for example SidX
If UCase(Mid(sFormula, pos2, 1)) = Chr(i) Then
isNamedRangePresent = False
Exit For
End If
Next i
'~~> "Sid
If isNamedRangePresent = True Then _
If UCase(Mid(sFormula, pos2, 1)) = Chr(34) Then isNamedRangePresent = False
'~~> _Sid
If isNamedRangePresent = True Then _
If UCase(Mid(sFormula, pos2, 1)) = Chr(95) Then isNamedRangePresent = False
End If
Loop
End Function
Output
PHEW!!!
This code creates a copy of the workbook with the names. It then goes through and deletes each name in your list of names from the that copied workbook. It counts up the number of formula errors in the workbook before and after. If the error count is the same, the name wasn't used. If it's different, the name was used.
I like to do this kind of test for really complicated situations like this. It means you don't have to worry so much about complicated rules for testing. You can just base your answer on the results.
Since the testing is all done on a copy, it should be safe. Be sure to save all your work before though!
To use, put put your list of names in a workbook and name the range with that list "NamesToTest":
Then put this code in the same workbook and run it:
Sub CheckNameUsage()
Dim WorkbookWithList As Excel.Workbook
Dim WorkbookWithNames As Excel.Workbook
Dim TempWb As Excel.Workbook
Dim cell As Excel.Range
Dim NameToCheck As String
Dim ws As Excel.Worksheet
Dim ErrorRange As Excel.Range
Dim ErrorsBefore As Long
Dim ErrorsAfter As Long
Dim NameUsed As Boolean
Set WorkbookWithList = ThisWorkbook
Set WorkbookWithNames = Workbooks("SO - wb to test.xlsx") 'adjust to suit
WorkbookWithNames.Worksheets.Copy 'Workbooks.Add(WorkbookWithNames.FullName)
Set TempWb = ActiveWorkbook
For Each cell In WorkbookWithList.Names("NamesToTest").RefersToRange.Cells
NameToCheck = cell.Value
ErrorsBefore = 0
For Each ws In TempWb.Worksheets
Set ErrorRange = Nothing
On Error Resume Next
Set ErrorRange = ws.Cells.SpecialCells(xlCellTypeFormulas, 16)
On Error GoTo 0
If Not ErrorRange Is Nothing Then
ErrorsBefore = ErrorsBefore + ErrorRange.Cells.Count
End If
Next ws
TempWb.Names(NameToCheck).Delete
ErrorsAfter = 0
For Each ws In TempWb.Worksheets
Set ErrorRange = Nothing
On Error Resume Next
Set ErrorRange = ws.Cells.SpecialCells(xlCellTypeFormulas, 16)
On Error GoTo 0
If Not ErrorRange Is Nothing Then
ErrorsAfter = ErrorsAfter + ErrorRange.Cells.Count
End If
Next ws
NameUsed = True
If ErrorsBefore = ErrorsAfter Then
NameUsed = False
End If
Debug.Print NameToCheck; " - Errors Before = " & ErrorsBefore; ", Errors After = " & ErrorsAfter; ", Used = " & NameUsed; ""
Next cell
TempWb.Close False
End Sub
The results will show in the Debug window:
The code is hopefully fairly self-explanatory. SpecialCells is worth knowing about, so read up on it if necessary. In this case it identifies cells with errors - that's the 16 argument.
Note that this only checks for workbook-level names. You could add checks for worksheet-level if necessary.
The following code works for me. The interesting points are
1) You can use the method range.ShowDependents to draw arrows to cells that are dependent on that range. When you are done, use range.ShowDependents True to remove the arrows.
2) Once the arrows are drawn, range.NavigateArrow can follow those arrows, and return the resulting range. I was unable to find any documentation on what happens if there are no dependent ranges. By experimenting, I was able to determine, that it will return the original range if there are no dependents.
Sub test_for_dependents(nm As Name)
Dim nm_rng As Range, result As Range
Dim i As Long
Set nm_rng = nm.RefersToRange
nm_rng.ShowDependents
Set result = nm_rng.NavigateArrow(False, 1, 1)
If result.Parent.Name = nm_rng.Parent.Name And result.Row = nm_rng.Row _
And result.Column = nm_rng.Column Then
MsgBox "Named range """ & nm.Name & """ isn't used!"
End If
nm_rng.ShowDependents True
Set nm_rng = Nothing
Set result = Nothing
End Sub
Sub test_all_names()
Dim nm As Name
Dim sht As Worksheet
For Each nm In ThisWorkbook.Names
test_for_dependents nm
Next nm
For Each sht In ThisWorkbook.Sheets
For Each nm In sht.Names
test_for_dependents nm
Next nm
Next sht
Set nm = Nothing
Set sht = Nothing
End Sub
The following NamesInCells macro reports the number of formula cells referencing each defined name (named range) in the active workbook. Results are in columns A:D (Scope, Name, RefersTo, Cells) starting at row 1 of the workbook's NamesInCells worksheet. If that worksheet does not exist, it will be added after the last sheet.
For each Name that is Visible (not hidden), the macro uses Private Function Formula_Errors to determine how many formula cells have errors before and after the Name's RefersTo property is made invalid. The before and after difference is the number of cells referencing that Name in a formula. However, if a Name is used in a cell formula that produced an error before, the after result will be the same for that cell. This issue is resolved by Private Function Prior_Errors which determines if the Name appears in an error cell's formula before the Name was made invalid. The InStr method used by Prior_Errors is imperfect, but only for formulas that had errors before initiating the macro (hopefully few). Also, a Name with workbook scope and a duplicate Name with sheet scope might be extraneously counted if they are in separate formulas that had initial errors.
This macro was inspired by Doug Glancy's answer above: https://stackoverflow.com/a/26691025/10172433
Public Sub NamesInCells()
Const myName As String = "NamesInCells"
Dim WB As Workbook, oName As Name, A() As Variant, vCells As Variant
Dim sScope As String, sName As String, sRefersTo As String
Dim nRows As Long, nR As Long, nBase As Long, n As Integer
Set WB = ActiveWorkbook
nRows = WB.Names.Count
If nRows = 0 Then
MsgBox "There are no defined names in the active workbook", _
vbInformation, myName
Exit Sub
End If
nRows = nRows + 1
ReDim A(1 To 4, 1 To nRows)
nR = 1
A(1, 1) = "Scope"
A(2, 1) = "Name"
A(3, 1) = "RefersTo"
A(4, 1) = "Cells"
nBase = Formula_Errors(WB)
For Each oName In WB.Names
With oName
If .Visible Then 'skip hidden names
n = InStrRev(.Name, "!")
If n = 0 Then
sScope = "Workbook"
sName = .Name
ElseIf n > 1 Then
sScope = Left(.Name, (n - 1))
sName = Mid(.Name, (n + 1))
End If
sRefersTo = .RefersTo
If Left(sScope, 1) = "'" Then _
sScope = Mid(sScope, 2, (Len(sScope) - 2))
.RefersTo = "#REF!"
vCells = Formula_Errors(WB) - nBase
.RefersTo = sRefersTo
vCells = vCells + Prior_Errors(WB, .Name)
nR = nR + 1
A(1, nR) = sScope
A(2, nR) = sName
A(3, nR) = "'" & sRefersTo
A(4, nR) = vCells
End If
End With
Next oName
If nR < 2 Then
MsgBox "There are no visible defined names in the active workbook", _
vbInformation, myName
Exit Sub
ElseIf nR < nRows Then
ReDim Preserve A(1 To 4, 1 To nR)
End If
On Error Resume Next
With WB
.Worksheets(myName).Activate
If Err = 0 Then
Range("A:D").Clear
Else
.Worksheets.Add After:=.Sheets(.Sheets.Count)
ActiveSheet.Name = myName
End If
End With
On Error GoTo 0
Range("A1").Select
Selection.Resize(nR, 4).Value = Application.Transpose(A)
End Sub
Private Function Formula_Errors(WB As Workbook) As Long
Dim WS As Worksheet, R As Range, nCount As Long
For Each WS In WB.Worksheets
On Error Resume Next
Set R = WS.Cells.SpecialCells(xlCellTypeFormulas, xlErrors)
If Err = 0 Then nCount = nCount + R.Count
On Error GoTo 0
Next WS
Formula_Errors = nCount
End Function
Private Function Prior_Errors(WB As Workbook, Name As String) As Long
Dim WS As Worksheet, R As Range, rCell As Range, nCount As Long
Dim sWS As String, sN As String, sF As String, n As Integer
n = InStrRev(Name, "!")
If n > 1 Then
sN = Mid(Name, (n + 1))
sWS = Left(Name, (n - 1))
If Left(sWS, 1) = "'" Then sWS = Mid(sWS, 2, (Len(sWS) - 2))
End If
For Each WS In WB.Worksheets
On Error Resume Next
Set R = WS.Cells.SpecialCells(xlCellTypeFormulas, xlErrors)
If Err = 0 Then
For Each rCell In R
sF = rCell.Formula
If WS.Name = sWS Then
If InStr(1, sF, sN, vbBinaryCompare) > 0 Then
nCount = nCount + 1
End If
ElseIf InStr(1, sF, Name, vbBinaryCompare) > 0 Then
nCount = nCount + 1
End If
Next rCell
End If
On Error GoTo 0
Next WS
Prior_Errors = nCount
End Function

Find where named ranges are being used in big workbook

I have a list of 594 named ranges in a workbook with nearly 20 sheets, each sheet has about 200 columns of data. I need to find out where the named ranges are being used so as to remove irrelevant ones. I pasted a list of named ranges onto the sheet and then I tried to find if they were used in a formula by recording them, and then using the find method in all sheets and columns. The problem is despite using lookin xlformulas, it retrieves the named range even if it is just a text.
Here is my (updated) attempt (if it is not evident already, i am an amateur):
Application.ScreenUpdating = False
Count = ActiveWorkbook.Sheets.Count
Sheets(Count).Activate
Dim locr(1 To 595)
Dim locc(1 To 595)
Dim locn(1 To 595)
Dim nam(1 To 595)
Dim rng As Range
Range("a1").Select
For X = 1 To 595 'populate array with named ranges
ActiveCell.Offset(1, 0).Select
nam(X) = ActiveCell.Value
Next X
For i = 1 To 595 'name loop
For j = 1 To (Count - 1) 'sheet loop
Sheets(j).Activate
On Error Resume Next
Set orange = Sheets(j).Cells.SpecialCells(xlCellTypeFormulas) 'limit range to cells that only contain formulas
On Error GoTo 20 'if no formulas in sheet, go to next sheet
If Not orange Is Nothing Then
Set rng = orange.Find(What:=nam(i), _
LookIn:=xlFormulas, _
LookAt:=xlPart, _
SearchOrder:=xlByRows, _
SearchDirection:=xlNext, _
MatchCase:=False) 'find named range
If Not rng Is Nothing Then 'if named range found
Application.Goto rng, True 'go to cell where name range found and record address
locr(i) = ActiveCell.Row
locc(i) = ActiveCell.Column
locn(i) = ActiveSheet.Name
GoTo 10 'value found, go to next sheet
Else
End If
Else
End If
20 Next j
locr(i) = "" 'record empty since "rng" is empty
locr(i) = ""
locr(i) = ""
10 Next i
Sheets(Count).Activate
Range("c1").Select
b = 1
For a = 1 To 595 'populate addresses of named ranges
ActiveCell.Offset(b, 2).Value = locr(a)
ActiveCell.Offset(b, 1).Value = locc(a)
ActiveCell.Offset(b, 0).Value = locn(a)
b = b + 1
Next a
Here is one way I can think of. I will explain this in 2 parts.
PART 1
Let's say we have a named range Sid.
This word Sid can appear in any one of these forms as shown in the image below. Why does it start with =? That has been explained in Part2 below.
=Sid '<~~ 1
="Sid" '<~~ 2
=XSid '<~~ 3
=SidX '<~~ 4
=_Sid '<~~ 5
=Sid_ '<~~ 6
=(Sid) '<~~ 7
Any other scenarios, I guess will be a subset of the above. Now out of these the only valid find in our case is the first one and the last one since we are looking for our named range.
So here is a quick function to check if the cell formula has a named range or not. I am sure it can be made more efficient
Function isNamedRangePresent(rng As Range, s As String) As Boolean
Dim sFormula As String
Dim pos1 As Long, pos2 As Long, sLen As Long, i As Long
sFormula = rng.Formula: sLen = Len(sFormula)
pos2 = 1
Do
pos1 = InStr(pos2, sFormula, s) - 1
If pos1 < 1 Then Exit Do
isNamedRangePresent = True
For i = 65 To 90
'~~> A-Z before Sid for example XSid
If UCase(Mid(sFormula, pos1, 1)) = Chr(i) Then
isNamedRangePresent = False
Exit For
End If
Next i
'~~> Check for " for example "Sid
If isNamedRangePresent = True Then _
If UCase(Mid(sFormula, pos1, 1)) = Chr(34) Then isNamedRangePresent = False
'~~> Check for underscore for example _Sid
If isNamedRangePresent = True Then _
If UCase(Mid(sFormula, pos1, 1)) = Chr(95) Then isNamedRangePresent = False
pos2 = pos1 + Len(s) + 1
If pos2 <= sLen Then
For i = 65 To 90
'~~> A-Z after Sid for example SidX
If UCase(Mid(sFormula, pos2, 1)) = Chr(i) Then
isNamedRangePresent = False
Exit For
End If
Next i
'~~> "Sid
If isNamedRangePresent = True Then _
If UCase(Mid(sFormula, pos2, 1)) = Chr(34) Then isNamedRangePresent = False
'~~> _Sid
If isNamedRangePresent = True Then _
If UCase(Mid(sFormula, pos2, 1)) = Chr(95) Then isNamedRangePresent = False
End If
Loop
End Function
So in the first and the last case, Debug.Print isNamedRangePresent(Range("D2"), "Sid") will give you True See this
PART 2
Now coming to the .Find. I see that you are searching only once in the worksheet. Since you can have many scenarios of the word Sid being present, you cannot just have one .Find. You will have to use .FindNext. See THIS link on how to use that. I have explained it there so I won't bother explaining that here.
We can make our .Find more efficient by searching only those cells which has formulas. To do that we have to use .SpecialCells(xlCellTypeFormulas). This explains why we had "=" in our example in PART1. :)
Here is an example (PART1 Code added at the bottom)
Sub Sample()
Dim oRange As Range, aCell As Range, bCell As Range
Dim oSht As Worksheet
Dim strSearch As String, FoundAt As String
Set oSht = Worksheets("Sheet1")
'~~> Set your range where you need to find - Only Formula Cells
On Error Resume Next
Set oRange = oSht.Cells.SpecialCells(xlCellTypeFormulas)
On Error GoTo 0
If Not oRange Is Nothing Then
strSearch = "Sid"
Set aCell = oRange.Find(What:=strSearch, LookIn:=xlFormulas, _
LookAt:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, _
MatchCase:=False, SearchFormat:=False)
If Not aCell Is Nothing Then
Set bCell = aCell
'~~> Check if the cell has named range
If isNamedRangePresent(aCell, strSearch) Then FoundAt = aCell.Address
Do
Set aCell = oRange.FindNext(After:=aCell)
If Not aCell Is Nothing Then
If aCell.Address = bCell.Address Then Exit Do
'~~> Check if the cell has named range
If isNamedRangePresent(aCell, strSearch) Then FoundAt = FoundAt & ", " & aCell.Address
Else
Exit Do
End If
Loop
Else
MsgBox SearchString & " not Found"
Exit Sub
End If
If FoundAt = "" Then
MsgBox "The Named Range was not found"
Else
MsgBox "The Named Range has been found these locations: " & FoundAt
End If
End If
End Sub
Function isNamedRangePresent(rng As Range, s As String) As Boolean
Dim sFormula As String
Dim pos1 As Long, pos2 As Long, sLen As Long, i As Long
sFormula = rng.Formula: sLen = Len(sFormula)
pos2 = 1
Do
pos1 = InStr(pos2, sFormula, s) - 1
If pos1 < 1 Then Exit Do
isNamedRangePresent = True
For i = 65 To 90
'~~> A-Z before Sid for example XSid
If UCase(Mid(sFormula, pos1, 1)) = Chr(i) Then
isNamedRangePresent = False
Exit For
End If
Next i
'~~> Check for " for example "Sid
If isNamedRangePresent = True Then _
If UCase(Mid(sFormula, pos1, 1)) = Chr(34) Then isNamedRangePresent = False
'~~> Check for underscore for example _Sid
If isNamedRangePresent = True Then _
If UCase(Mid(sFormula, pos1, 1)) = Chr(95) Then isNamedRangePresent = False
pos2 = pos1 + Len(s) + 1
If pos2 <= sLen Then
For i = 65 To 90
'~~> A-Z after Sid for example SidX
If UCase(Mid(sFormula, pos2, 1)) = Chr(i) Then
isNamedRangePresent = False
Exit For
End If
Next i
'~~> "Sid
If isNamedRangePresent = True Then _
If UCase(Mid(sFormula, pos2, 1)) = Chr(34) Then isNamedRangePresent = False
'~~> _Sid
If isNamedRangePresent = True Then _
If UCase(Mid(sFormula, pos2, 1)) = Chr(95) Then isNamedRangePresent = False
End If
Loop
End Function
Output
PHEW!!!
This code creates a copy of the workbook with the names. It then goes through and deletes each name in your list of names from the that copied workbook. It counts up the number of formula errors in the workbook before and after. If the error count is the same, the name wasn't used. If it's different, the name was used.
I like to do this kind of test for really complicated situations like this. It means you don't have to worry so much about complicated rules for testing. You can just base your answer on the results.
Since the testing is all done on a copy, it should be safe. Be sure to save all your work before though!
To use, put put your list of names in a workbook and name the range with that list "NamesToTest":
Then put this code in the same workbook and run it:
Sub CheckNameUsage()
Dim WorkbookWithList As Excel.Workbook
Dim WorkbookWithNames As Excel.Workbook
Dim TempWb As Excel.Workbook
Dim cell As Excel.Range
Dim NameToCheck As String
Dim ws As Excel.Worksheet
Dim ErrorRange As Excel.Range
Dim ErrorsBefore As Long
Dim ErrorsAfter As Long
Dim NameUsed As Boolean
Set WorkbookWithList = ThisWorkbook
Set WorkbookWithNames = Workbooks("SO - wb to test.xlsx") 'adjust to suit
WorkbookWithNames.Worksheets.Copy 'Workbooks.Add(WorkbookWithNames.FullName)
Set TempWb = ActiveWorkbook
For Each cell In WorkbookWithList.Names("NamesToTest").RefersToRange.Cells
NameToCheck = cell.Value
ErrorsBefore = 0
For Each ws In TempWb.Worksheets
Set ErrorRange = Nothing
On Error Resume Next
Set ErrorRange = ws.Cells.SpecialCells(xlCellTypeFormulas, 16)
On Error GoTo 0
If Not ErrorRange Is Nothing Then
ErrorsBefore = ErrorsBefore + ErrorRange.Cells.Count
End If
Next ws
TempWb.Names(NameToCheck).Delete
ErrorsAfter = 0
For Each ws In TempWb.Worksheets
Set ErrorRange = Nothing
On Error Resume Next
Set ErrorRange = ws.Cells.SpecialCells(xlCellTypeFormulas, 16)
On Error GoTo 0
If Not ErrorRange Is Nothing Then
ErrorsAfter = ErrorsAfter + ErrorRange.Cells.Count
End If
Next ws
NameUsed = True
If ErrorsBefore = ErrorsAfter Then
NameUsed = False
End If
Debug.Print NameToCheck; " - Errors Before = " & ErrorsBefore; ", Errors After = " & ErrorsAfter; ", Used = " & NameUsed; ""
Next cell
TempWb.Close False
End Sub
The results will show in the Debug window:
The code is hopefully fairly self-explanatory. SpecialCells is worth knowing about, so read up on it if necessary. In this case it identifies cells with errors - that's the 16 argument.
Note that this only checks for workbook-level names. You could add checks for worksheet-level if necessary.
The following code works for me. The interesting points are
1) You can use the method range.ShowDependents to draw arrows to cells that are dependent on that range. When you are done, use range.ShowDependents True to remove the arrows.
2) Once the arrows are drawn, range.NavigateArrow can follow those arrows, and return the resulting range. I was unable to find any documentation on what happens if there are no dependent ranges. By experimenting, I was able to determine, that it will return the original range if there are no dependents.
Sub test_for_dependents(nm As Name)
Dim nm_rng As Range, result As Range
Dim i As Long
Set nm_rng = nm.RefersToRange
nm_rng.ShowDependents
Set result = nm_rng.NavigateArrow(False, 1, 1)
If result.Parent.Name = nm_rng.Parent.Name And result.Row = nm_rng.Row _
And result.Column = nm_rng.Column Then
MsgBox "Named range """ & nm.Name & """ isn't used!"
End If
nm_rng.ShowDependents True
Set nm_rng = Nothing
Set result = Nothing
End Sub
Sub test_all_names()
Dim nm As Name
Dim sht As Worksheet
For Each nm In ThisWorkbook.Names
test_for_dependents nm
Next nm
For Each sht In ThisWorkbook.Sheets
For Each nm In sht.Names
test_for_dependents nm
Next nm
Next sht
Set nm = Nothing
Set sht = Nothing
End Sub
The following NamesInCells macro reports the number of formula cells referencing each defined name (named range) in the active workbook. Results are in columns A:D (Scope, Name, RefersTo, Cells) starting at row 1 of the workbook's NamesInCells worksheet. If that worksheet does not exist, it will be added after the last sheet.
For each Name that is Visible (not hidden), the macro uses Private Function Formula_Errors to determine how many formula cells have errors before and after the Name's RefersTo property is made invalid. The before and after difference is the number of cells referencing that Name in a formula. However, if a Name is used in a cell formula that produced an error before, the after result will be the same for that cell. This issue is resolved by Private Function Prior_Errors which determines if the Name appears in an error cell's formula before the Name was made invalid. The InStr method used by Prior_Errors is imperfect, but only for formulas that had errors before initiating the macro (hopefully few). Also, a Name with workbook scope and a duplicate Name with sheet scope might be extraneously counted if they are in separate formulas that had initial errors.
This macro was inspired by Doug Glancy's answer above: https://stackoverflow.com/a/26691025/10172433
Public Sub NamesInCells()
Const myName As String = "NamesInCells"
Dim WB As Workbook, oName As Name, A() As Variant, vCells As Variant
Dim sScope As String, sName As String, sRefersTo As String
Dim nRows As Long, nR As Long, nBase As Long, n As Integer
Set WB = ActiveWorkbook
nRows = WB.Names.Count
If nRows = 0 Then
MsgBox "There are no defined names in the active workbook", _
vbInformation, myName
Exit Sub
End If
nRows = nRows + 1
ReDim A(1 To 4, 1 To nRows)
nR = 1
A(1, 1) = "Scope"
A(2, 1) = "Name"
A(3, 1) = "RefersTo"
A(4, 1) = "Cells"
nBase = Formula_Errors(WB)
For Each oName In WB.Names
With oName
If .Visible Then 'skip hidden names
n = InStrRev(.Name, "!")
If n = 0 Then
sScope = "Workbook"
sName = .Name
ElseIf n > 1 Then
sScope = Left(.Name, (n - 1))
sName = Mid(.Name, (n + 1))
End If
sRefersTo = .RefersTo
If Left(sScope, 1) = "'" Then _
sScope = Mid(sScope, 2, (Len(sScope) - 2))
.RefersTo = "#REF!"
vCells = Formula_Errors(WB) - nBase
.RefersTo = sRefersTo
vCells = vCells + Prior_Errors(WB, .Name)
nR = nR + 1
A(1, nR) = sScope
A(2, nR) = sName
A(3, nR) = "'" & sRefersTo
A(4, nR) = vCells
End If
End With
Next oName
If nR < 2 Then
MsgBox "There are no visible defined names in the active workbook", _
vbInformation, myName
Exit Sub
ElseIf nR < nRows Then
ReDim Preserve A(1 To 4, 1 To nR)
End If
On Error Resume Next
With WB
.Worksheets(myName).Activate
If Err = 0 Then
Range("A:D").Clear
Else
.Worksheets.Add After:=.Sheets(.Sheets.Count)
ActiveSheet.Name = myName
End If
End With
On Error GoTo 0
Range("A1").Select
Selection.Resize(nR, 4).Value = Application.Transpose(A)
End Sub
Private Function Formula_Errors(WB As Workbook) As Long
Dim WS As Worksheet, R As Range, nCount As Long
For Each WS In WB.Worksheets
On Error Resume Next
Set R = WS.Cells.SpecialCells(xlCellTypeFormulas, xlErrors)
If Err = 0 Then nCount = nCount + R.Count
On Error GoTo 0
Next WS
Formula_Errors = nCount
End Function
Private Function Prior_Errors(WB As Workbook, Name As String) As Long
Dim WS As Worksheet, R As Range, rCell As Range, nCount As Long
Dim sWS As String, sN As String, sF As String, n As Integer
n = InStrRev(Name, "!")
If n > 1 Then
sN = Mid(Name, (n + 1))
sWS = Left(Name, (n - 1))
If Left(sWS, 1) = "'" Then sWS = Mid(sWS, 2, (Len(sWS) - 2))
End If
For Each WS In WB.Worksheets
On Error Resume Next
Set R = WS.Cells.SpecialCells(xlCellTypeFormulas, xlErrors)
If Err = 0 Then
For Each rCell In R
sF = rCell.Formula
If WS.Name = sWS Then
If InStr(1, sF, sN, vbBinaryCompare) > 0 Then
nCount = nCount + 1
End If
ElseIf InStr(1, sF, Name, vbBinaryCompare) > 0 Then
nCount = nCount + 1
End If
Next rCell
End If
On Error GoTo 0
Next WS
Prior_Errors = nCount
End Function

Resources