Finding key words in Excel with VBA - excel

The file I am working on is picking the selected words from all the comments, colors them and segregates them into the dedicated tabs.
All keywords have been coded into the macro itself. Instead of writing the keywords to the macro, I want to tell the macro the keywords are located in an array in an excel sheet so everybody can use the file according to their needs.
When I made below changes for keywords to an array, I am getting below error on the screenshot that I do not know why.
Satellite:
KeyW = Array("Satellite", "image", "blacks out", "resolution")
Satellite:
KeyW = Array(Worksheets("MAIN").Range("N5:N15"))
The code below was not written by me. I just made some modifications.
Error that I am getting:
runtime error 13, Type mismatch
when I click debug it shows this yellow line
Sub sort()
Dim KeyW()
Dim cnt_Rows As Long, cnt_Columns As Long, curr_Row As Long, i As Long, x As Long
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False
Sheets(Array("Television", "Satellite", "News", "Sports", "Movies", "Key2", "Key3", "Error", "Commercial", "Key4", "TV", "Key5", "Key6", "Signal", "Key1", "Key7", "Design", "Hardware")).Select
Satellite:
KeyW = Array("Satellite", "image", "blacks out", "resolution")
KeyWLen = UBound(KeyW, 1)
j = 2
For i = 0 To KeyWLen
With Worksheets(1).Range("c4:e7000")
Set c = .Find(KeyW(i), LookIn:=xlValues, LookAt:=xlPart)
If Not c Is Nothing Then
firstAddress = c.Address
Do
Sheets("Satellite").Range("b" & j).Value = Worksheets(1).Range("a" & c.Row).Value
Worksheets(1).Range(c.Address).Copy
Sheets("Satellite").Activate
Range("a" & j).Select
Selection.PasteSpecial Paste:=xlPasteAllUsingSourceTheme, Operation:=xlNone _
, SkipBlanks:=False, Transpose:=False
Range("a" & j).Select
WordPos = 1
StartPos = 1
SearchStr = KeyW(i)
While WordPos <> 0
WordPos = InStr(StartPos + 1, Range("a" & j).Value, SearchStr, 1)
If WordPos > 0 _
Then
With ActiveCell.Characters(Start:=WordPos, Length:=Len(SearchStr)).Font
.FontStyle = "Bold"
.Color = -16727809
End With
StartPos = WordPos
End If
Wend
Worksheets(1).Activate
j = j + 1
Set c = .FindNext(c)
Loop While Not c Is Nothing And c.Address <> firstAddress
End If
End With
Next i

I'd start by splitting out some of the logic into standalone methods, and calling them from your main code: this makes it easier to see what's going on and allows some re-use of your code later on.
For example:
Sub sort()
Dim wb As Workbook
Dim txt As String, allCells As Collection, c As Range, w As Range, rngDest As Range
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False
Set wb = ThisWorkbook
'(removed sheet selection code - not needed here)
Set rngDest = wb.Worksheets("Satellite").Range("A2") 'start listing matches here
For Each w In wb.Worksheets("MAIN").Range("N5:N15").Cells 'loop over possible search terms
txt = Trim(w.Value)
If Len(txt) > 0 Then
Set allCells = FindAll(wb.Worksheets(1).Range("c4:e7000"), txt) 'get all matches
For Each c In allCells
c.Copy rngDest 'copy matched cell
BoldWord rngDest, txt 'bold matched text
rngDest.Offset(0, 1) = _
c.EntireRow.Columns("A").Value 'copy colA from matched cell
Set rngDest = rngDest.Offset(1) 'next result row
Next c
End If
Next w
End Sub
'return a Collection of all cells in `rng` which contain `txt`
Public Function FindAll(rng As Range, txt As String) As Collection
Dim rv As New Collection, f As Range
Dim addr As String
Set f = rng.Find(what:=txt, after:=rng.Cells(rng.Cells.Count), _
LookIn:=xlValues, LookAt:=xlPart, SearchOrder:=xlByRows, _
SearchDirection:=xlNext, MatchCase:=False)
If Not f Is Nothing Then addr = f.Address()
Do Until f Is Nothing
rv.Add f
Set f = rng.FindNext(after:=f)
If f.Address() = addr Then Exit Do
Loop
Set FindAll = rv
End Function
'Bold all instances of `wrd` in cell `c`
Sub BoldWord(c As Range, txt As String)
Dim pos As Long, start As Long
start = 1
Do
pos = InStr(start, c.Value, txt, vbTextCompare)
If pos = 0 Then Exit Do
With c.Characters(pos, Len(txt))
.Font.Bold = True
.Font.Color = vbRed
End With
start = pos + Len(txt)
Loop
End Sub

Related

Search for all values between 2 values in a column and loop till last one found

Lets start with I am self taught in Excel VBA and have a question that might seem stupid or basic:
I have the following information on a sheet:
[ConfBlastPlan]
DRB1065
PU1962;427;05_37_OB;A;2;2;1
PU1963;364;05_37_OB;B;2;2;1
PU1959;373;05_37_OB;C;2;2;1
-
[FiringProcedure]11:55:21;MULTI
What I want to do is combine all strings between with "PU" and the first ";" that is found between the
"[ConfBlastPlan]" and [FiringProcedure] into one cell.
I have read up about the loop function but seems I have confused myself terribly.
How do I loop this and combine the strings found?
I have started the function using the following code:
Sub DRBEquipNumberPU() 'GET THE PU#s
Dim WSFrom As Worksheet
Dim WSTo As Worksheet
Dim RngFrom As Range
Dim RngTo As Range
Dim BlastNumber As String
Dim BlastNumberStep As Long
Dim SearchString As String
Dim SearchStringStart As String
Dim SearchStringEnd As String
Dim LineStep As Long
Dim Blastedrng As Range
Dim BlastedFoundrng As Range
Dim closePos As Integer
BlastNumberStep = 1
LineStep = 1
Set Blastedrng = ThisWorkbook.Worksheets("Blast Summary Sheet").Range("A2", Range("A2").End(xlDown))
For Each BlastedFoundrng In Blastedrng.Cells
On Error Resume Next
SearchString = "[ConfBlastPlan]"
SearchStringStart = "PU"
SearchStringEnd = "[FiringProcedure]"
BlastNumber = CStr("Blasted " & BlastNumberStep)
Set WSFrom = Worksheets(CStr(BlastNumber))
Set RngFrom = WSFrom.Cells.Find(What:=SearchString, LookIn:=xlFormulas, _
LookAt:=xlPart, SearchOrder:=xlByRows, _
SearchDirection:=xlNext, MatchCase:=False, SearchFormat:=False)
Set RngFrom1 = WSFrom.Cells.Find(What:=SearchStringStart, After:=RngFrom, LookIn:=xlFormulas, _
LookAt:=xlPart, SearchOrder:=xlByRows, _
SearchDirection:=xlNext, MatchCase:=False, SearchFormat:=False)
Set WSTo = ThisWorkbook.Worksheets("Blast Summary Sheet")
Set RngTo = WSTo.Cells.Find(What:=(CStr(BlastNumber)), LookIn:=xlFormulas, _
LookAt:=xlPart, SearchOrder:=xlByRows, _
SearchDirection:=xlNext, MatchCase:=False, SearchFormat:=False)
closePos = InStr(1, RngFrom.Cells.Value, ";")
If RngTo.Cells.Offset(0, 4).Value = "INCOMPLT" Then
RngTo.Cells.Offset(0, 7).Value = "INCOMPLT"
ElseIf RngFrom.Cells.Value Is Nothing Then
RngTo.Cells.Offset(0, 7).Value = "NO PU #s"
ElseIf RngFrom.Cells.Value Like SearchStringStart Then
RngTo.Cells.Offset(0, 7).Value = Mid(RngFrom.Cells.Value, 0, closePos)
ElseIf RngFrom.Cells.Value = SearchStringEnd Then
End If
BlastNumberStep = BlastNumberStep + 1
Next BlastedFoundrng
End Sub
All it returns at the moment is INCOMPL or NO PU #s
There can be a maximum of 48 instances of PU
Please help
Blasted 23:
Blasted 26:
Blasted 27:
Option Explicit
' Major changes: make it two steps-- 1)Get all Sheet names, 2)Process all Lines on one sheet
Sub StepThruBlastedSheetNames() 'GET THE PU#s
Dim WSSummary As Worksheet, rowSummary As Long
Set WSSummary = ThisWorkbook.Worksheets("Blast Summary Sheet")
rowSummary = 1
Dim WSFrom As Worksheet
For Each WSFrom In ThisWorkbook.Worksheets
If InStr(WSFrom.Name, "Blasted ") > 0 Then
StepThruBlastedLines WSSummary, rowSummary, WSFrom
End If
Next
End Sub
Sub StepThruBlastedLines(WSSummary As Worksheet, rowSummary As Long, WSFrom As Worksheet)
' these never change, ergo do not put inside loop
Const SearchStringStart As String = "[ConfBlastPlan]"
Const SearchStringFindPU As String = "PU"
Const SearchStringEnd As String = "[FiringProcedure]"
Dim rowFrom As Long
Dim rowMax As Long
rowMax = WSFrom.Cells(WSFrom.Rows.Count, "A").End(xlUp).Row
Dim IsBetween As String, PUlist As String, posSemi As Long, DRBname As String
IsBetween = "N"
PUlist = ""
DRBname = ""
For rowFrom = 1 To rowMax
If IsBetween = "Y" Then
If InStr(WSFrom.Cells(rowFrom, "A"), "DRB") > 0 Then
DRBname = WSFrom.Cells(rowFrom, "A")
End If
If InStr(WSFrom.Cells(rowFrom, "A"), SearchStringFindPU) > 0 Then
posSemi = InStr(WSFrom.Cells(rowFrom, "A"), ";")
PUlist = PUlist & Mid(WSFrom.Cells(rowFrom, "A"), 1, posSemi)
End If
If InStr(WSFrom.Cells(rowFrom, "A"), SearchStringEnd) > 0 Then
IsBetween = "N"
rowSummary = rowSummary + 1
WSSummary.Cells(rowSummary, "A") = WSFrom.Name
WSSummary.Cells(rowSummary, "B") = DRBname
If PUlist <> "" Then
WSSummary.Cells(rowSummary, "C") = PUlist
PUlist = ""
Else
'<< add put empty notice
WSSummary.Cells(rowSummary, "C") = "INCOMPL"
End If
DRBname = "" '<<added
End If
ElseIf WSFrom.Cells(rowFrom, "A") = SearchStringStart Then
IsBetween = "Y"
End If
Next rowFrom
End Sub
Here's code that extracts the PU-values from a worksheet like the one you posted. I couldn't figure out why you called this worksheet WsTo and perhaps that's the reason why I also couldn't guess at your intention for what to do with the result. Your question is mute on the point. So I left the project at that point. I'm sure you will be able to pick it up from the two ways I'm displaying the Output array.
Sub DRBEquipNumberPU()
' 134
' Get the PU#s
Const Blast As String = "[ConfBlastPlan]"
Const BlastEnd As String = "-"
Const Marker As String = "PU"
Dim WsTo As Worksheet
Dim BlastFound As Range
Dim CellVal As String ' loop variable: Cell.Value
Dim R As Long ' loop counter: rows
Dim Output As Variant ' array of found values
Dim i As Long ' index to Output
Set WsTo = ThisWorkbook.Worksheets("Blast Summary Sheet")
With WsTo.Columns(1)
Set BlastFound = .Find(What:=Blast, _
LookIn:=xlValues, _
Lookat:=xlWhole, _
MatchCase:=False)
If BlastFound Is Nothing Then
MsgBox """" & Blast & """ wasn't found.", _
vbInformation, "No data to process"
Else
ReDim Output(1 To 100) ' choose UBound larger than you ever need
R = BlastFound.Row
Do
R = R + 1
CellVal = .Cells(R).Value
If InStr(1, Trim(CellVal), Marker, vbTextCompare) = 1 Then
i = i + 1
Output(i) = CellVal
End If
Loop While Len(CellVal) And CellVal <> BlastEnd
If i Then
ReDim Preserve Output(1 To i)
MsgBox "Found values = " & vbCr & _
Join(Output, Chr(13))
For i = LBound(Output) To UBound(Output)
Debug.Print Output(i)
Next i
End If
End If
End With
End Sub
It just occurs to me that the end marker you suggested ("FiringProcedure]") may be more reliable than my choice ("-"). If so, just change it at the top of the code where the constants are declared. If that marker is missed the code might continue to include the "PU" line below the [Blasting Plan] row.

Exit from a "Find" infinite loop

I have created a Do Loop with Find to replace "Hello" with "Hi" inside column A of Sheet1, but only if the string "XYZ" is not in the same row of column B.
When Find does not replace "Hello", because in column B there is "XYZ", we enter an infinite loop since FindNext always finds "Hello" in column 1
It is possible to avoid infinite loop without making Loop While very complicated?
Please see this image of columns in sheet1
Sub CallMask()
Call Masks("Hello", "XYZ")
End Sub
Sub Masks(sMask_I As String, sNoReplace_I As String)
With Sheets("Sheet1").Columns(1)
Dim CellToReplace As Range
Set CellToReplace = .Find(What:=sMask_I, LookIn:=xlValues, _
SearchDirection:=xlNext, MatchCase:=True, Lookat:=xlPart)
If Not CellToReplace Is Nothing Then
Dim InitialAddress As String
InitialAddress = CellToReplace.Address
Dim MaskRow As Long
Dim Mask As String
On Error Resume Next
Do
MaskRow = WorksheetFunction.Match(sMask_I, _
Sheets("Sheet1").Range("C1:C" & Rows.Count), 0)
Mask = Sheets("Sheet1").Range("D" & MaskRow).Value2
If Sheets("Sheet1").Cells(CellToReplace.Row, 2) <> sNoReplace_I Then
CellToReplace.Value2 = Replace(CellToReplace.Value2, sMask_I, Mask)
End If
Set CellToReplace = .FindNext(CellToReplace)
Loop While Not CellToReplace Is Nothing And CellToReplace.Address _
<> InitialAddress
On Error GoTo 0
End If
End With
End Sub
You could try this:
Option Explicit
Sub CallMask()
Call Masks("Hello", "XYZ", "Hi")
End Sub
Sub Masks(sMask_I As String, sNoReplace_I As String, Replacement As String)
Dim C As Range
With ThisWorkbook.Sheets("Sheet1")
For Each C In .Range("A1", "A" & .Cells(.Rows.Count, 1).End(xlUp).Row)
If C Like "*" & sMask_I & "*" And C.Offset(0, 1) <> sNoReplace_I Then
C.Replace sMask_I, Replacement
End If
Next C
End With
End Sub
When using Find() in a loop it's typically easier to abstract that out into a separate method:
Sub CallMask()
Masks "Hello", "XYZ"
End Sub
Sub Masks(sMask_I As String, sNoReplace_I As String)
Dim matches As Collection, c
Set matches = FindAll(Sheets("Sheet1").Columns(1), sMask_I)
For Each c In matches
If c.Offset(0, 1) <> sNoReplace_I Then
c.Value = Replace(c.Value, sMask_I, c.Offset(0, 3).Value)
End If
Next c
End Sub
'return all matches as a collection
Public Function FindAll(rng As Range, val As String) As Collection
Dim rv As New Collection, f As Range
Dim addr As String
Set f = rng.Find(what:=val, after:=rng.Cells(rng.Cells.Count), _
LookIn:=xlValues, LookAt:=xlPart, SearchOrder:=xlByRows, _
SearchDirection:=xlNext, MatchCase:=False)
If Not f Is Nothing Then addr = f.Address()
Do Until f Is Nothing
rv.Add f
Set f = rng.FindNext(after:=f)
If f.Address() = addr Then Exit Do
Loop
Set FindAll = rv
End Function
I have tested with arrays as suggested by Damian, AJD and Mathieu. It is the fastest code.
Times for 1600 rows are:
My new code with arrays: 8 ms
Damian code with For Next: 132 ms
The Code with "separate method" of Tim Williams: 402 ms
My first code with Find: 511 ms
This is the new code:
Sub CallMask()
Call Masks("Hello", "XYZ")
End Sub
Sub Masks(ByVal sMask_I As String, ByVal sNoReplace_I As String)
With ThisWorkbook.Sheets("Sheet1")
Dim ArrayRangeToMask As Variant
ArrayRangeToMask = .Range("A1:B" & .Cells(.Rows.Count, 2).End(xlUp).Row)
Dim MaskRow As Long
Dim Mask As String
MaskRow = WorksheetFunction.Match(sMask_I, .Range("C1:C" & Rows.Count), 0)
Mask = .Range("D" & MaskRow).Value2
Dim RowMasking As Long
For RowMasking = 1 To UBound(ArrayRangeToMask)
If InStr(ArrayRangeToMask(RowMasking, 1), sMask_I) And _
ArrayRangeToMask(RowMasking, 2) <> sNoReplace_I Then
ArrayRangeToMask(RowMasking, 1) = _
Replace(ArrayRangeToMask(RowMasking, 1), sMask_I, Mask)
End If
Next RowMasking
.Range("A1:B" & .Cells(.Rows.Count, 2).End(xlUp).Row) = ArrayRangeToMask
End With
End Sub

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

Selection based on finding 2 different words in 2 columns

I would like to do the following using Excel VBA:
1) look for a certain word_1 within a column;
2) if word_1 was found in step (1), go one column to the right and look for another word which is called word_2. If word_2 was found as well, delete the entire row.
If on the other hand, word_2 was not found, the row does not have to be deleted.
The general idea is to search for multiple words in one column and if they are found, also double-check (for safety) if certain affiliated words are in column 2. Only then the entire rows should be deleted.
I made the following little example for testing:
Col1 Col2
xxx xxx
xxx xxx
xxx xxx
findme acg
xxx xxx
findme xxx
In this example I am searching for the word "findme" in column 1 and for the associated word "acg" in column 2. As you can see, row 4 would have to be deleted because both words occur in one row, as opposed to e.g. row 6, where this is not the case.
My final code:
Sub xxx()
Dim aCell As Range, bCell As Range, aSave As String
Dim fndOne As String, fndTwo As String
fndOne = "findme"
fndTwo = "acg"
Dim ws As Worksheet: Set ws = ActiveWorkbook.ActiveSheet
Application.DisplayAlerts = False
Application.ScreenUpdating = False
With ws
Set aCell = .Columns(1).Find(What:=fndOne, LookIn:=xlValues, _
lookat:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, _
MatchCase:=False, SearchFormat:=False)
If Not aCell Is Nothing Then
aSave = aCell.Address
Do
If LCase(.Cells(aCell.row, 2).Value) Like Chr(42) & fndTwo & Chr(42) Then
If bCell Is Nothing Then
Set bCell = .Range("A" & aCell.row)
Else
Set bCell = Union(bCell, .Range("A" & aCell.row))
End If
End If
Set aCell = .Columns(1).FindNext(After:=aCell)
Loop Until aCell.Address = aSave
End If
Set aCell = Nothing
If Not bCell Is Nothing Then bCell.EntireRow.Delete
End With
Application.DisplayAlerts = True
Application.ScreenUpdating = True
End Sub
If you used the Range.Find method and Range.FindNext method, deleting as you go and checking for matching records after each deletion, you should be able to loop through the possibilities quickly.
'delete rows as they are found
Sub delTwofers()
Dim rw As Long, n As Long, cnt As Long, rng As Range
Dim v As Long, sALLTERMs As String, vPAIRs As Variant, vTERMs As Variant
On Error GoTo bm_SafeExit
Application.ScreenUpdating = False
Application.EnableEvents = False
Application.Calculation = xlCalculationManual
Debug.Print Timer
sALLTERMs = "aa;bb|cc;dd|ee;ff"
With Worksheets("Sheet1") 'set this worksheet reference properly!
vPAIRs = Split(LCase(sALLTERMs), Chr(124))
For v = LBound(vPAIRs) To UBound(vPAIRs)
vTERMs = Split(vPAIRs(v), Chr(59))
cnt = Application.CountIfs(.Columns(1), Chr(42) & vTERMs(0) & Chr(42), .Columns(2), Chr(42) & vTERMs(1) & Chr(42))
rw = 1
For n = 1 To cnt
rw = .Columns(1).Find(what:=vTERMs(0), lookat:=xlPart, _
after:=.Columns(1).Cells(rw + (rw <> 1)), MatchCase:=False).Row
Do While True
If LCase(.Cells(rw, 2).Value2) Like Chr(42) & vTERMs(1) & Chr(42) Then
.Rows(rw).Delete
Exit Do
Else
rw = .Columns(1).FindNext(after:=.Cells(rw, 1)).Row
End If
Loop
Next n
Next v
End With
Debug.Print Timer
bm_SafeExit:
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
Application.EnableEvents = True
End Sub
'collect rows with Union, delete them all at once
Sub delTwofers2()
Dim rw As Long, n As Long, cnt As Long, rng As Range
Dim v As Long, sALLTERMs As String, vPAIRs As Variant, vTERMs As Variant
On Error GoTo bm_SafeExit
Application.ScreenUpdating = False
Application.EnableEvents = False
Application.Calculation = xlCalculationManual
Debug.Print Timer
sALLTERMs = "aa;bb|cc;dd|ee;ff"
With Worksheets("Sheet1") 'set this worksheet reference properly!
vPAIRs = Split(LCase(sALLTERMs), Chr(124))
For v = LBound(vPAIRs) To UBound(vPAIRs)
vTERMs = Split(vPAIRs(v), Chr(59))
cnt = Application.CountIfs(.Columns(1), Chr(42) & vTERMs(0) & Chr(42), .Columns(2), Chr(42) & vTERMs(1) & Chr(42))
rw = 1
For n = 1 To cnt
rw = .Columns(1).Find(what:=vTERMs(0), lookat:=xlPart, _
after:=.Columns(1).Cells(rw), MatchCase:=False).Row
Do While True
If LCase(.Cells(rw, 2).Value2) Like Chr(42) & vTERMs(1) & Chr(42) Then
If rng Is Nothing Then
Set rng = .Cells(rw, 1)
Else
Set rng = Union(rng, .Cells(rw, 1))
End If
Exit Do
Else
rw = .Columns(1).FindNext(after:=.Cells(rw, 1)).Row
End If
Loop
Next n
Next v
End With
Debug.Print Timer 'check timer before deleting discontiguous rows
If Not rng Is Nothing Then _
rng.EntireRow.Delete
Debug.Print Timer
bm_SafeExit:
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
Application.EnableEvents = True
End Sub
By first checking to make sure there is something to delete, some error control can be avoided; you only need to find the entry for the double matching criteria that you know exists.
Addendum: Deleting a collection of discontiguous rows is time consuming. The second routine (delTwofers2) above was 5% slower that the one that deleted rows as they were found. 25,000 values, 755 random deletions - 3.60 seconds for the first; 3.75 seconds for the latter.
This code applies a filter to the first two columns of the used range using your criteria. It then deletes the visible rows:
Sub DeleteSelected()
Dim RangeToFilter As Excel.Range
Set RangeToFilter = ActiveSheet.UsedRange
With RangeToFilter
.AutoFilter Field:=1, Criteria1:="find me"
.AutoFilter Field:=2, Criteria1:="access granted"
.SpecialCells(xlCellTypeVisible).EntireRow.Delete Shift:=xlUp
End With
End Sub

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