Infinite loop with .Find method - vba

I am trying to write a VBA script to automate moving things around in an spreadsheet that has a balance sheet imported from an accounting software.
The values on imported balance sheet start at row 5, column A has some text describing what the values of each row mean, and columns B and D have the amounts for each item.
The subtotals for each section and subsection of the balance sheet are on columns C and E. Each subtotal has is in a cell formatted with a solid upper border.
I would like to bring all these subtotals to the same columns as the values (i.e, columns B and D). I've tried to do this using the .Find method to search for cells with the specific format (cells with an upper border) and a Do loop to keep searching until I find all cells that should have a subtotal in it.
Notes:
I didn't use FindNext because it seems that it ignores format settings used in the preceding Find method, as described here.
I tried to used the FindAll function described by Tushar Mehta to go around this problem with FindNext, but it didn't find all cells with the specified format.
Here's the code. Any help is greatly appreciated!
Sub FixBalanceSheet()
Dim LookFor As Range
Dim FoundHere As String 'Address of the cell that should contain a subtotal
Dim beginAt As Range, endAt As Range, rng As Range 'Set the ranges for the sum to get the subtotal
Dim place As String 'String with the address of a cell that will contain a subtotal
Dim WhereToLook As Range 'Range where subtotals are to be found
'Set workbook and worksheet
With Sheets("Sheet1")
Set WhereToLook = Range("A5:F100")
'Every cell containing a subtotal has an upper border. So, look for cells containing border!
With Application.FindFormat.Borders(xlEdgeBottom)
.LineStyle = xlContinuous
.ColorIndex = xlAutomatic
.TintAndShade = 0
End With
'Call search using .Find
Set LookFor = WhereToLook.Find(What:="", After:=Cells(5, 2), _
LookIn:=xlFormulas, LookAt:=xlPart, _
SearchOrder:=xlByRows, SearchDirection:=xlNext, _
MatchCase:=False, SearchFormat:=True)
If Not LookFor Is Nothing Then 'Test if a cell with a bottom border is found
'What happens when a subtotal cell is found:
FoundHere = LookFor.Address
Debug.Print "Found at: " & Found
'Loop to set a range, sum values and put them in the right cell
Do
'% find out a range to calculate subtotals and put the value in the right cells %'
'Call for next search
With Application.FindFormat.Borders(xlEdgeBottom)
.LineStyle = xlContinuous
.ColorIndex = xlAutomatic
.TintAndShade = 0
End With
Set LookFor = WhereToLook.Find(What:="", After:=endAt, SearchFormat:=True)
Debug.Print "LookFor now is: " & LookFor.Address
Rem If LookFor.Address = Found Then ' Do not allow wrapped search
Rem Exit Do
Rem End If
Loop Until LookFor Is Nothing Or LookFor.Address = FoundHere ' Do not allow wrapped search
End If
End With
End Sub

Consider using a range object to loop through your range. You can add a total if you need a grand total, but this is probably an easier way than trying to select all cells that have formatting.
For example:
Sub TestStackOverflowCode()
Dim r As Range
Dim rngToChk As Range
'This is where you'd insert WhereToLook
Set rngToChk = ActiveSheet.Range("B1:B4")
For Each r In rngToChk
'If the top edge does not NOT have a border
If r.Borders(xlEdgeTop).LineStyle <> xlNone Then
'Copy the cell value to two cells to the right
r.Offset(, 2).Value = r.Value
End If
Next r
End Sub

I would recommend going back to the Range.Find/Range.FindNext method. There were some holes in your logic conditions and I believe I've adjusted them.
Set LookFor = WhereToLook.Find(What:="", After:=Cells(5, 2), _
LookIn:=xlFormulas, LookAt:=xlPart, _
SearchOrder:=xlByRows, SearchDirection:=xlNext, _
MatchCase:=False, SearchFormat:=True)
If Not LookFor Is Nothing Then 'Test if a cell with a bottom border is found
'What happens when a subtotal cell is found:
FoundHere = LookFor.Address
Debug.Print "Found at: " & FoundHere
'Loop to set a range, sum values and put them in the right cell
Do
'do something with LookFor as a Range Object here
'Call for next search
Set LookFor = WhereToLook.FindNext(After:=LookFor) '<~~ look for next after current cell
Debug.Print "LookFor now is: " & LookFor.Address
Loop Until LookFor.Address = FoundHere ' Do not allow wrapped search (LookFor will never be nothing here)
End If

The findNext may not have worked if
you had [FindFormat.Borders...] after the [Set LookFor = WhereToLook.Find(...]
I do think ThreeTrickPony's answer is more elegant, but in general I'd suggest finding an alternative way to identify cells rather than formatting.

Related

Changing numbers in each column into the substring of each column title

Using Excel, I'm trying to accomplish the following:
Sample of before & after
I need to convert all the 1's into the corresponding country name of each column. With a small dataset like the sample above, it is easy to do it manually, but not so easy when I have 196 columns x 2 datasets. Any suggestions would be much appreciated!
Duplicate the sheet and clear all the values except the header in the second sheet. Put the following formula in every corresponding cell except the header row in the second sheet.
=IF(INDIRECT(ADDRESS(ROW(),COLUMN(),1,1,"Sheet1"),TRUE)=1,
MID(INDIRECT(ADDRESS(1,COLUMN())),
FIND("[",INDIRECT(ADDRESS(1,COLUMN())))+1,
FIND("]",INDIRECT(ADDRESS(1,COLUMN())))-
FIND("[",INDIRECT(ADDRESS(1,COLUMN())))-1),"")
You can then copy the corresponding cells of the second sheet. Past values into the first sheet. You're done.
{You'll want to remove carriage returns from the formula. Also, you might want to delete the second sheet to hide the magic. :-) }
If you're willing to use vba instead of a formula, I think that this will work for you. It looks for non-empty cells within "A:F" and for those cells it grabs the name inside of [...]
Sub AdjustName()
Dim cell As Range
For Each cell In Range("A2:F" & LastRowInRange_Find(Range("A:F")))
If Len(cell.Value2) > 0 Then
With Cells(1, cell.Column)
cell.Value2 = Mid(.Value2, InStr(.Value2, "[") + 1, Len(.Value2) - 1 - InStr(.Value2, "["))
End With
End If
Next cell
End Sub
Function LastRowInRange_Find(ByVal rng As Range) As Long
'searches range from bottom up looking for "*" (anything)
Dim rngFind As Range
Set rngFind = rng.Find( _
What:="*", _
After:=Cells(rng.row, rng.Column), _
LookAt:=xlWhole, _
LookIn:=xlValues, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious)
If Not rngFind Is Nothing Then
LastRowInRange_Find = rngFind.row
Else
LastRowInRange_Find = rng.row
End If
End Function

Excel VBA - Searching in a Loop

First, my code (below) works, but I am trying to see if it can be simplified. The macro in which this code is located will have a lot of specific search items and I want to make it as efficient as possible.
It is searching for records with a specific category (in this case "Chemistry") then copying those records into another workbook. I feel like using Activate in the search, and using Select when moving to the next cell are taking too much time and resources, but I don't know how to code it to where it doesn't have to do that.
Here are the specifics:
Search column T for "Chemistry"
Once it finds "Chemistry", set that row as the "top" record. e.g. A65
Move to the next row down, and if that cell contains "Chemistry", move to the next row (the cells that contain "Chemistry" will all be together"
Keep going until it doesn't find "Chemistry", then move up one row
Set that row for the "bottom" record. e.g. AX128
Combine the top and bottom rows to get the range to select. e.g. A65:AX128
Copy that range and paste it into another workbook
Here is the code:
'find "Chemistry"
Range("T1").Select
Cells.Find(What:="Chemistry", After:=ActiveCell, LookIn:=xlFormulas, LookAt _
:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:= _
False, SearchFormat:=False).Activate
'set top row for selection
toprow = ActiveCell.Row
topcellselect = "A" & toprow
'find all rows for Chemistry
Do While ActiveCell = "Chemistry"
ActiveCell.Offset(1, 0).Select
Loop
ActiveCell.Offset(-1, 0).Select
'set bottom row for selection
bottomrow = ActiveCell.Row
bottomcellselect = "AX" & bottomrow
'define selection range from top and bottom rows
selectionrange = topcellselect & ":" & bottomcellselect
'copy selection range
Range(selectionrange).Copy
'paste into appropriate sheet
wb1.Activate
Sheets("Chemistry").Select
Range("A2").PasteSpecial
Thanks in advance for any help!
You never need to select or activate unless that's really what you want to do (at the end of the code, if you want the user to see a certain range selected). To remove them, just take out the activations and selections, and put the things on the same line. Example:
wb1.Activate
Sheets("Chemistry").Select
Range("A2").PasteSpecial
Becomes
wb1.Sheets("Chemistry").Range("A2").PasteSpecial
For the whole code; I just loop thorugh the column and see where it starts and stops being "chemistry". I put it in a Sub so you only have to call the sub, saying which word you're looking for and where to Paste it.
Sub tester
Call Paster("Chemistry", "A2")
End sub
Sub Paster(searchWord as string, rngPaste as string)
Dim i as integer
Dim startRange as integer , endRange as integer
Dim rng as Range
With wb1.Sheets("Chemistry")
For i = 1 to .Cells(Rows.Count,20).End(XlUp).Row
If .Range("T" & i ) = searchWord then 'Here it notes the row where we first find the search word
startRange = i
Do until .Range("T" & i ) <> searchWord
i = i + 1 'Here it notes the first time it stops being that search word
Loop
endRange = i - 1 'Backtracking by 1 because it does it once too many times
Exit for
End if
Next
'Your range goes from startRange to endRange now
set rng = .Range("T" & startRange & ":T" & endRange)
rng.Copy
.Range(rngPaste).PasteSpecial 'Paste it to the address you gave as a String
End with
End sub
As you can see I put the long worksheet reference in a With to shorten it. If you have any questions or if it doesn't work, write it in comments (I haven't tested)
The most efficient way is to create a Temporary Custom Sort Order and apply it to your table.
Sub MoveSearchWordToTop(KeyWord As String)
Dim DestinationWorkSheet As Workbook
Dim SortKey As Range, rList As Range
Set SortKey = Range("T1")
Set rList = SortKey.CurrentRegion
Application.AddCustomList Array(KeyWord)
rList.Sort Key1:=SortKey, Order1:=xlAscending, Header:=xlGuess, _
OrderCustom:=Application.CustomListCount + 1, MatchCase:=False, _
Orientation:=xlTopToBottom, DataOption1:=xlSortNormal
Application.DeleteCustomList Application.CustomListCount
Set DestinationWorkSheet = Workbooks("Some Other Workbook.xlsx").Worksheets("Sheet1")
rList.Copy DestinationWorkSheet.Range("A1")
End Sub

Excel Macro multiple range concatenation

I am trying to concatenate cells through excel VBA. This involves multiple ranges. Below is my table
Degree1
Course1,Course2,Course3
Course4,course5,course6
Degree2
Course1,Course2
Course3,Course4
Course5
Course6,Course7
Degree3
Course1,Course2,Course3
Course4,course5,course6
Course7
I want to concatenate all the courses listed below a degree into a single cell next to the degree. Each degree has multiple courses & the # of rows differ for each degree.
I am using excel find function to identify the cell contains the degree & select the courses below it. I am also using the concat function from http://www.contextures.com/rickrothsteinexcelvbatext.html so that I can concatenate the selected ranges.
I tried to write the below code but this is not working, I am getting value error in the end. I guess the range is not stored in the variable
Sub concatrange()
Dim D1Crng As Range 'to set courses under degree1 as range
Dim D2Crng As Range
Dim D3Crng As Range
Dim D1cell As Range 'to identify the cell of D1 and set it as range
Dim D2cell As Range
Dim D3cell As Range
Range("A1:B100").Select
Selection.Find(What:="Degree1", _
LookIn:=xlValues, LookAt:=xlPart, _
SearchOrder:=xlByColumns, SearchDirection:=xlNext, _
MatchCase:=False, SearchFormat:=False).Select
ActiveCell.Select
Set D1cell = Selection
Range(D1cell).Activate
ActiveCell.Offset(1, 0).End(xlDown).Select
Set D1Crng = Selection
Range(D1cell).Activate
ActiveCell.Offset(0, 1).Select
Selection.Formula = "=concat("","",D1Crng)"
End sub
I am repeating the above process for concatenating for other degrees.
VBA's .Join command should work well here.
Sub many_degrees()
Dim rw As Long
With ActiveSheet
For rw = 1 To .Cells(Rows.Count, 1).End(xlUp).Row
If LCase(Left(.Cells(rw, 1).Value, 6)) = "degree" Then
If Application.CountA(.Cells(rw, 1).Resize(3, 1)) > 2 Then
.Cells(rw, 2) = Join(Application.Transpose(.Range(.Cells(rw, 1).Offset(1, 0), .Cells(rw, 1).End(xlDown)).Value), Chr(44))
Else
.Cells(rw, 2) = .Cells(rw, 1).Offset(1, 0).Value
End If
End If
Next rw
End With
End Sub
I have accounted for the case where only one (or none) line of degrees exists below the DegreesX title. The code does depend upon each 'title' starting with Degree as the first 6 characters (not case sensitive). I've used .Offset(x, y) where a simple +1 to the row or column probably would have sufficed, but that may help in understanding the purpose of the various code lines.
      

Checking values in a column to change multiple row values

I have a spreadsheet with a good amount of data (more than 100,000 rows with columns a-h).
I know twenty or more distinct values in Column F are wrong and I know what they should be. For example, every occurrence of "pif" should be "pig" and "coe" should be "cow", etc.
(Note that there are multiple incorrect values (i.e. multiple "pif"s) for each.)
I'm currently building a macro to go through and fix these individually, and that part works:
Sub FixColumnF()
ActiveSheet.Columns("F").Replace What:= _
"pif", _
Replacement:="pig", LookAt:=xlPart, SearchOrder:=xlByRows, MatchCase:= _
False, SearchFormat:=False, ReplaceFormat:=False
ActiveSheet.Columns("F").Replace What:= _
"coe", _
Replacement:="cow", LookAt:=xlPart, SearchOrder:=xlByRows, MatchCase:= _
False, SearchFormat:=False, ReplaceFormat:=False
...
End Sub
My problem is that column A is used to keep track of errors, one of which is an incorrect value in column F. How do I erase the value in column A to indicate there is no longer an error for each row where the value in column F is fixed?
I'm extremely new to vba, so any help would be very much appreciated!
Siddharth posted his suggestions while I was testing/typing mine. My first suggestion is a simple variation of his first suggestion and offers no advantage that I can see. However, my second suggestion is different from either of his suggestions and may be appropriate.
You need to loop for each occurrence of "pif" and "coe" if you want to do something extra with each faulty value found. The code below shows how to replace every occurrence of "pif" by "pig" and then do something with column "A". If you like this technique, you would need to duplicate this code for "coe" and "cow".
Option Explicit
Sub ReplaceFaultyA()
Dim Rng As Range
' It is best to avoid "ActiveSheet" in case the use has called the
' macro with the wrong worksheet active. Probably this is not likely
' in this case but I like to follow good practice even if it not not
' necessary
With Worksheets("Data")
Do While True
' Are you sure all your Find parameters are correct? For example,
' "LookAt:=xlPart" means than "pif" in the middle of a word will
' be replaced by "pig". "LookAt:=xlWhole" may better match your
' requirement. I suggest you look up the specification of Find
' and consider the implications of each parameter.
Set Rng = .Columns("F").Find(What:="pif")
If Rng Is Nothing Then
' No [more] occurrences of "pif" in column F
Exit Do
Else
' Occurrences of "pif" found in column F
.Cells(Rng.Row, "F") = "pig"
' Amend column ""A" of Rng.Row as necessary
End If
Loop
End With
End Sub
Duplication my loop and replacing "pif" and "pig" by "coe" and "cow" in the duplicate is probably the simpliest solution if there are only two replacements. However, if there are many replacements, the technique below may be a better choice.
In this code, I place the faulty values and the matching good values in arrays. With this approach, one block of replacement code can handle an indefinite number of replacements providing the action for column A is the same for each replacement.
Sub ReplaceFaultyB()
Dim InxValue As Long
Dim Rng As Range
Dim ValueFaulty() As Variant
Dim ValueGood() As Variant
' The code below assumes there same number of entries in both arrays.
' You can add extra entries to the two arrays as necessary.
' This assumes the action for column "A" is the same for each faulty value
ValueFaulty = Array("pif", "coe", "appme")
ValueGood = Array("pig", "cow", "apple")
With Worksheets("Data")
For InxValue = LBound(ValueFaulty) To UBound(ValueFaulty)
Do While True
Set Rng = .Columns("F").Find(What:=ValueFaulty(InxValue))
If Rng Is Nothing Then
' No [more] occurrences of this faulty value in column F
Exit Do
Else
' Occurrences of faulty value found in column F
.Cells(Rng.Row, "F") = ValueGood(InxValue)
' Amend column ""A" of Rng.Row as necessary
End If
Loop
Next InxValue
End With
End Sub
There are two ways that I can think of.
Way 1
Use .Find/.FindNext to loop though all the cells which have say the word "pif". In that loop not only replace the word but also edit the value in Col A. For example (Untested)
Sub Sample()
Dim oRange As Range, aCell As Range, bCell As Range
Dim ws As Worksheet
Dim SearchString As String, FoundAt As String
On Error GoTo Whoa
Set ws = Worksheets("Sheet1")
Set oRange = ws.Columns(5) '<~~ Column 5
SearchString = "pif"
NewString = "pig"
Set aCell = oRange.Find(What:=SearchString, LookIn:=xlValues, _
LookAt:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, _
MatchCase:=False, SearchFormat:=False)
If Not aCell Is Nothing Then
Set bCell = aCell
'~~> Change value in Cell F
aCell.Value = Replace(aCell.Value, SearchString, NewString)
'~~> Change value in Cell A
aCell.Offset(, -5).Value = "Rectified"
Do
Set aCell = oRange.FindNext(After:=aCell)
If Not aCell Is Nothing Then
If aCell.Address = bCell.Address Then Exit Do
aCell.Value = Replace(aCell.Value, SearchString, NewString)
aCell.Offset(, -5).Value = "Rectified"
Else
Exit Do
End If
Loop
End If
Exit Sub
Whoa:
MsgBox Err.Description
End Sub
Way 2
Use Autofilter to filter Col F on the word "pif". Loop though all the cells in Col A and add the relevant message. Remove Autofilter and then run your original code (mentioned in your question) to do the replace in Col F. see THIS link on how to use Autofilter.

VBA searching through rows and their associated columns and hide column if there is nothing

I am new to VBA programming. I would like to
search through the worksheet, and find "N" or "TR" on row 6
Then, For every cell in the column of "N" or "TR"
if all the cells are blank, then delete/ hide the column
if the cells are not blank, highlight the cells that are in blank
This sounds easy but I think it requires two for loops.
Sub checkandhide()
Set r = Range("6:6")
Rows("7:7").Select
For Each Cell In r
Selection.Find(What:="N", After:=ActiveCell, LookIn:=xlFormulas, LookAt _
:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:= _
False, MatchByte:=False, SearchFormat:=False).Activate
'search for N
Application.Run "hidecolumn"
Next
End Sub
Sub hidecolumn()
Dim target As Range
Dim dwn As Range
Set dwn = Range(ActiveCell.End(xlDown).Address)
ActiveCell.Select
ActiveCell.Offset(6, 0).Select
For Each Cell In dwn
If Cell.Text = "" Then Columns.Delete
Next
End Sub
attached example spreadsheet
You don't need two loops.
You mentioned you want to hide column but your code suggest you delete it (I kept solution which hides)
You didn't mentioned which is empty range (which cells are blank) to decide to hide the column- I assumed everything below 11th row.
Here is the code which is tried and tested with some comments inside it.
Sub checkandhide()
Dim r As Range
Dim Cell As Range
'don't run it for the complete row but from first to last cell in it
Set r = Range("A6", Cells(6, Columns.Count).End(xlToLeft))
For Each Cell In r
'you don't need find if you simply need to check value of the cell
'let's assume we check for 'N' & 'TR' but not 'n' or 'tr'
If Cell.Value = "N" Or Cell.Value = "TR" Then
'there are few possibilities to check if there is any value below _
row 11 (?!) in analysed column. I would use this one:
If Cells(Rows.Count, Cell.Column).End(xlUp).Row < 12 Then
Cell.EntireColumn.Hidden = True
End If
End If
Next
End Sub