Merge cells and delete duplicate data - vba

I have a list of companies and each has a scope of work, address and phone number. Some of the companies have multiple scopes of work. It looks something like this:
I want to get rid of the second copy of the stuff like the address (and in my case phone numbers and such) while copying the unique data in the second line and putting it in the first line and then getting rid of the second line.
I have very little experience of coding. I looked up how to do this step by step but something is wrong within the code or the syntax:
I found code for going down a column for a blank space.
I looked up how I would copy a cell to the right of the active blank cell.
I found code for merging the info into the cell one above and one to the right of the active cell.
I found code that deletes the row with the active cell.
I want it to loop until there are no more blank company cells.
So this is how I put it together:
Public Sub SelectFirstBlankCell()
Dim sourceCol As Integer, rowCount As Integer, currentRow As Integer
Dim currentRowValue As String
Do
sourceCol = 6 'column F has a value of 6
rowCount = Cells(Rows.Count, sourceCol).End(xlUp).Row
'for every row, find the first blank cell and select it
For currentRow = 1 To rowCount
currentRowValue = Cells(currentRow, sourceCol).Value
If IsEmpty(currentRowValue) Or currentRowValue = "" Then
Cells(currentRow, sourceCol).Select
End If
Next
Loop Until A647
End Sub
.
Sub mergeIt()
Range(ActiveCell.Offset(0, 1), ActiveCell.Offset(1, 1)).Merge
ActiveCell.Select
End Sub
.
Sub DeleteRow()
RowNo = ActiveCell.Row
If RowNo < 7 Then Exit Sub
Range("A" & ActiveCell.Row).EntireRow.Delete
Sheets("Summary").Select
Range("A4:O4").Select
Selection.Copy
LastRow = Range("A65536").End(xlUp).Offset(1, 0).Row
End Sub

Please never post code as an image since someone who wants to try it out must type it. You can edit your question and add a new section including revised code if necessary.
My copy of your code (plus line numbers) is:
1 Public Sub SelectFirstBlankCell()
2 Dim sourceCol As Integer, rowCount As Integer, currentRow As Integer
3 Dim currentRowValue As String
4 sourceCol = 1 'column F has a value of 6
5 rowCount = Cells(Rows.Count, sourceCol).End(xlUp).Row
6 'for every row, find the first blank cell and select it
7 For currentRow = 1 To rowCount
8 currentRowValue = Cells(currentRow, sourceCol).Value
9 If IsEmpty(currentRowValue) Or currentRowValue = "" Then
10 Cells(currentRow, sourceCol).Select
11 End If
12 Range(ActiveCell.Offset(0, 0), ActiveCell.Offset(1, 1)).Merge
13 ActiveCell.Select
14 If IsEmpty(currentRowValue) Or currentRowValue = "" Then
15 Cells(Range("sourceCol:21")).Delete
16 End If
17 Next
18 End Sub
I am sure we all started selecting cells and accessing the ActiveCell because the macro recorder does this. However, selecting cells is slow and it is very easy to lose track of what is selected. I believe this is your main problem.
Problem 1 The end value for a For-Loop is fixed at the start; Any attempt to reduce rowCount when you delete something will have no effect on the For-Loop.
Problem 2 I suspect you mean the range in line 15 to be sourceCol & ":" & currentRow.
Problem 3 In line 10 you select a cell if it is blank. In line 12 you merge the active cell whether or not you have just selected it. This means your code attempts a merge for every row.
Problem 4 Column 1 is the column that might be blank. Suppose row 1000 is the last row with a supplier's name but row 1005 is the last row with a product. Your code would not process rows 1001 to 1005.
Problem 5 Function IsEmpty() only returns sensible values for Variants. A Variant is either a cell or a variable that can hold different types of value.
I have not tried your code so there may be more mistakes. Do get dispirited. To the best of my knowledge, problem 1 is not documented. I had to discover this "feature" for myself by attempting code similar to yours. The specification for Function IsEmpty() states its limitations but, unless you fully understand Variants, the significance is not obvious. The other problems are easy errors to make and only practice will reduce their frequency.
Below is my solution to your problem. It is not how I would code it for myself but I think I have introduced enough new concepts for one solution.
I do not say much about the syntax of the VBA statements I use since it is usually easy to look up a statement once you know it exists. Ask if necessary but please try to understand the code before asking.
I do not like deleting in situ; it is slow and, if your code is faulty, you have to load the previous version of the worksheet and start again. I have a source (Src) and a Destination (Dest) worksheet.
I use constants for values that might change but not during a single run of your macro.
You assume the address and other details for Jan's Supply on rows 2 and 3 match. I am paranoid and never make assumptions like this. If my code would discard important information if rows 2 and 3 did not match, I check they match. I also allow for rows like this because I have encountered them:
John's supply Cookies 555 Main Street CA
Cakes Littleville CA
This will become:
John's supply Cookies & Cakes 555 Main Street Littleville CA
Some of the comments explain my choice of VBA statement but most do not. When you have to update a macro you wrote 12 months ago for new requirements, the few minutes you spent adding comments can save you hours finding your way around the code.
You may not like my system of naming variables. Fine; develop your own. When you return to this macro in 12 months, an immediate understanding of the variables will save more time.
Option Explicit
Const WkshtSrcName As String = "Sheet1" ' \ Replace "Sheet1" and "Sheet2"
Const WkshtDestName As String = "Sheet2" ' / with the names of your worksheets
Const ColSupplier As String = "A" ' \ In Cells(R, C), C can be a
Const ColProduct As String = "B" ' / number or a column identifier
Const RowDataFirst As Long = 1
Sub MergeRowsForSameSupplier()
Dim ColCrnt As Long ' \ Columns in source and destination are the
Dim ColMax As Long ' / same so single variables are adequate.
Dim RowDestCrnt As Long ' \ Rows in source and destination
Dim RowSrcCrnt As Long ' | worksheets are different
Dim RowSrcMax As Long ' / so need separate variables.
Dim ProductCrnt As String
Dim Join As String
Dim SupplierCrnt As String
Dim WkshtSrc As Worksheet
Dim WkshtDest As Worksheet
Set WkshtSrc = Worksheets(WkshtSrcName)
Set WkshtDest = Worksheets(WkshtDestName)
With WkshtSrc
' I consider this to be the easiest technique of identifying the last used
' row and column in a worksheet. Note: the used range includes trailing
' rows and columns that are formatted but otherwise unused or were used but
' aren't now so other techniques can better match what the user or the
' programmer usually mean by "used".
ColMax = .UsedRange.Columns.Count
RowSrcMax = .UsedRange.Rows.Count
End With
With WkshtDest
.Cells.EntireRow.Delete ' Delete any existing contents
End With
RowDestCrnt = RowDataFirst
For RowSrcCrnt = RowDataFirst To RowSrcMax
With WkshtSrc
SupplierCrnt = .Cells(RowSrcCrnt, ColSupplier).Value
ProductCrnt = .Cells(RowSrcCrnt, ColProduct).Value
End With
If SupplierCrnt <> "" Then
' This is the first or only row for a supplier.
' Copy it to Destination worksheet.
With WkshtSrc
.Range(.Cells(RowSrcCrnt, 1), .Cells(RowSrcCrnt, ColMax)).Copy _
Destination:=WkshtDest.Cells(RowDestCrnt, 1)
End With
RowDestCrnt = RowDestCrnt + 1
ElseIf ProductCrnt = "" Then
' Both Supplier and Product cells are empty.
With WkshtSrc
If .Cells(RowSrcCrnt, Columns.Count).End(xlToLeft).Column = 1 And _
.Cells(RowSrcCrnt, 1).Value = "" And _
.Cells(RowSrcCrnt, Columns.Count).Value = "" Then
' If you do not understand why I have so many tests,
' experiment with Ctrl+Left
' Row empty so ignore it
Else
' Don't know what to do with this error so give up
Call MsgBox("Cells " & ColSupplier & RowSrcCrnt & " and " & _
ColProduct & RowSrcCrnt & " of worksheet " & _
WkshtSrcName & _
" are blank but the entire row is not blank", _
vbOKOnly + vbCritical, "Merge rows for same supplier")
Exit Sub
End If
End With
Else
' Supplier cell is empty. Product cell is not.
' Row RowDestCrnt-1 of the Destination worksheet contains the first row
' for this supplier or the result of merging previous rows for this
' supplier.
If WkshtSrc.Cells(RowSrcCrnt + 1, ColSupplier).Value = "" And _
WkshtSrc.Cells(RowSrcCrnt + 1, ColProduct).Value <> "" Then
' The next row is for the same supplier but is not a blank row
Join = ","
Else
' This is last row for this supplier
Join = " &"
End If
' Add to list of products
With WkshtDest
.Cells(RowDestCrnt - 1, ColProduct).Value = _
.Cells(RowDestCrnt - 1, ColProduct).Value & Join & " " & _
ProductCrnt
End With
For ColCrnt = 1 To ColMax
If ColCrnt = Cells(1, ColSupplier).Column Or _
ColCrnt = Cells(1, ColProduct).Column Then
' You may think (and you may be right) that the supplier and product
' will always be in the first two columns. But have seen the
' weirdest arrangements and make no assumptions
' Ignore this column
Else
If WkshtSrc.Cells(RowSrcCrnt, ColCrnt).Value = "" Then
' The most likely arrangement: the subsequent row has no
' value in this column. Nothing to do.
ElseIf WkshtDest.Cells(RowDestCrnt - 1, ColCrnt).Value = "" Then
' This source row has a value in this column but [the] previous
' row[s] did not.
' Note: I use the copy statement because it copies formatting as
' well as the value which may be useful.
WkshtSrc.Cells(RowSrcCrnt, ColCrnt).Copy _
Destination:=WkshtDest.Cells(RowDestCrnt - 1, ColCrnt)
ElseIf WkshtSrc.Cells(RowSrcCrnt, ColCrnt).Value = _
WkshtDest.Cells(RowDestCrnt - 1, ColCrnt).Value Then
' Values match. Nothing to do.
Else
' Values do not match.
' Don't know what to do with this error so give up.
Call MsgBox("The value in cell " & ColNumToCode(ColCrnt) & _
RowSrcCrnt & " of worksheet " & WkshtSrcName & _
" does not match a value in an earlier row " & _
"for the same supplier", _
vbOKOnly + vbCritical, "Merge rows for same supplier")
Exit Sub
End If
End If
Next
End If
Next
With WkshtDest
.Cells.Columns.AutoFit
End With
End Sub
Function ColNumToCode(ByVal ColNum As Long) As String
' Convert a column identifier (A, AA, etc.) to its number
Dim Code As String
Dim PartNum As Long
' Last updated 3 Feb 12. Adapted to handle three character codes.
If ColNum = 0 Then
ColNumToCode = "0"
Else
Code = ""
Do While ColNum > 0
PartNum = (ColNum - 1) Mod 26
Code = Chr(65 + PartNum) & Code
ColNum = (ColNum - PartNum - 1) \ 26
Loop
End If
ColNumToCode = Code
End Function

Related

How to Assign values to varying range of cells in VBA

I am trying randomly generate a whole number between 1 and 100, whether that be in a cell or in the vba code directly. Then I want to use that value as the lookup value for a VLookup that will pull another randomly generated whole number between 1 and 10 from a different sheet. Then I want to use that second number between 1 and 10 as an indicator to fill in that many cells in a column with the first number between 1 and 100.
So for example if I were doing it manually: I would have in cell "C27" on Sheet1 =MROUND(RANDBETWEEN(1,100),1). Let's say it returns 40. Then I would look on Sheet2 for number 40 in column A, look over to Column D where there is another =MROUND(RANDBETWEEN(1,10),1). Let's say that one returns 5 (so I need to fill in 5 cells of a column). Then I would head back to Sheet1 and enter 40 into cells K31 through K35 (the original random whole number).
I'm aware that RAND and RANDBETWEEN update anytime the worksheet recalculates. I use triggered IF statements to keep them from updating unless I change a value in a trigger cell. If generating a random number with VBA makes that even easier, I'm all for it.
I don't think it will be helpful for me to post the many iterations I've attempted as I've tried to apply solutions to each individual task of this macro. None of them have seemingly even gotten me close. But here's what I'm using right now that's also not even close. This code was for me to try and get it to work period. So the numbers are static and not random. But I need them random. And yes, this is for me to generate random monsters for my D&D game mastering :)
Thanks to anyone who might be able to get me on the right track!
Sub MonsterRoll()
'
' MonsterRoll
Dim ws As Worksheet
Dim roll As Integer
Dim No1 As Integer
Dim No2 As Integer
Set ws = Sheets("Combat Helper")
roll = 5
No1 = 31
No2 = 31 + 5
On Error Resume Next
For i = No1 To No2
area.Cells(i, 11).Value = 5
Next
End Sub
This table houses the vlookups into sheet "Encounters"
This table contains the source data, with column D being a RANDBETWEEN
I'm still not sure about a few cell references, but think I have a general idea. The code below can be a starting point to do most of what you want -- with a few warnings...
Since you are monitoring for changes in Sheet1 cells K31:K50, and then making changes to that same range, that will trigger the change event again. So, to avoid crazy results, I added a flag so that it will ignore changes untill you tell it to stop ignoring. That will be when you have finished all processing for your original change.
Personally, I would prefer to generate my own random numbers via code for the simple reason that ANY change to any cell will trigger all of your 'random' numbers to regenerate.
Go to Function 'Set_All_Cell_Values' and add whatever code you need to fill other cells.
Option Explicit
Dim blnIgnoreChanges As Boolean
Private Sub Worksheet_Change(ByVal Target As Range)
Dim ws1 As Worksheet
Dim ws2 As Worksheet
Dim i As Integer
Dim iYourNbr As Integer
Dim iMyNbr As Integer
Dim iRow As Integer
Dim iHowMany As Integer
Dim Why As String
' The following code can be dangerous if your code is not working properly!!!!
' Since you want to 'monitor' changes to K31:K50, and then change those same cells via code,
' which will in turn trigger this 'Worksheet_Change' subroutine to fire again,
' you need to be able to ignore changes on demand.
' If this flag gets set and your code didn't complete (AND turn the flag off), then
' any monitoring of future changes will be ignored!!
' If the flag fails to get reset, then just execute the following code in the immediate window:
' blnIgnoreChanges = false
If blnIgnoreChanges = True Then
Exit Sub
End If
Set ws1 = ThisWorkbook.Worksheets("Combat Helper")
Set ws2 = ThisWorkbook.Worksheets("Encounters")
' Sample data in Sheet2
' A B C D E F G H I J
'40 Bird, Falcon 1 1 1 -10 5 2 1d4 t
'41 Men: Wild Man 2 3 2 -9 2 3 1d5 u
'42 Beast 3 5 3 -8 3 4 1d6 v
'43 Elephant 4 7 4 -7 4 5 1d7 w
' Monitor only cells K31:K50
If Target.Row >= 31 And Target.Row <= 50 And Target.Column = 11 Then
' Value must be between 1 and 100
If Target.Value < 1 Or Target.Value > 100 Then
MsgBox "Must enter between 1 and 100"
Exit Sub
Else
' If you want to Lookup match in Col A of Sheet2, and then get value from col D.
iYourNbr = Application.VLookup(Target.Value, ws2.Range("A3:N102"), 4, False)
' I prefer to Generate my own random number between 1 and 10
iMyNbr = Int((10 - 1 + 1) * Rnd + 1)
iRow = Find_Matching_Value(Target.Value)
Debug.Print "Matching Row in Sheet2 is: " & iRow
' DANGER!! If you execute the following line of code, then you MUST set to FALSE
' when you have finished one change!!!
blnIgnoreChanges = True
iHowMany = Sheet2.Cells(iRow, 4).Value
Sheet1.Cells(Target.Row, 13) = iHowMany
Set_All_Cell_Values Target.Row, iRow, iHowMany
End If
' We can ignore all other cell changes
Else
'Debug.Print "Change made to: " & "R" & Target.Row & ":C" & Target.Column & " but not my row or column! Value is:" & Target.Value
End If
End Sub
Function Set_All_Cell_Values(iS1Row As Integer, iS2Row As Integer, iHowMany As Integer)
Dim i As Integer
Debug.Print "Add code to set cells for Sheet1 R:" & iS1Row & " Sheet2 R:" & iS2Row
For i = iS1Row + 1 To iS1Row + iHowMany - 1
Sheet1.Cells(i, 11) = Sheet1.Cells(iS1Row, 11)
'#################################################
' ADD CODE TO FILL OTHER CELLS as needed!!!
'#################################################
Next i
blnIgnoreChanges = False
End Function
Function Find_Matching_Value(iFind As Integer) As Integer
Dim Rng As Range
If Trim(iFind) <> "" Then
With Sheets("Encounters").Range("A:A")
Set Rng = .Find(What:=iFind, _
After:=.Cells(.Cells.Count), _
LookIn:=xlValues, _
LookAt:=xlWhole, _
SearchOrder:=xlByRows, _
SearchDirection:=xlNext, _
MatchCase:=False)
If Not Rng Is Nothing Then
Find_Matching_Value = Rng.Row
Else
MsgBox "Did not find match for value: " & iFind
End If
End With
Else
MsgBox "You passed an empty value to 'Find_Matching_Value'"
End If
End Function

Excel VBA - struggling to create macros which takes action on cell values by column

Very new to Excel VBA, and struggling.
I'm a junior C# developer, but am finding that writing simple statements in VBA is very tricky for me. Can anyone tell me how to write VBA code for the following pseudocode requirements please?
Insert True into Column E only WHERE there is a specific string value of X in column A
Insert False into Column E WHERE there is text (ie: something/anything) in column D AND no text (ie: nothing) in Column A
Delete X wherever there is a specific string value X in any cell in Column A.
Any help at all would be so greatly appreciated.
Public Sub Text_Process()
Dim lngLastRow As Long
Dim lngRow As Long
Dim strColA As String
Dim strColD As String
lngLastRow = Sheet1.UsedRange.Rows.Count
For lngRow = 1 To lngLastRow ' change 1 to 2 if you have headings in row 1
strColA = Sheet1.Cells(lngRow, 1).Value ' store value in column A
strColD = Sheet1.Cells(lngRow, 4).Value ' store value of column D
Sheet1.Cells(lngRow, 5).Clear ' clear column E
If strColA = "X" Then ' or whatever you are looking for
Sheet1.Cells(lngRow, 5).Value = True
ElseIf strColA = "" And strColD <> "" Then
Sheet1.Cells(lngRow, 5).Value = False
End If
If strColA = "X" Then ' or whatever you are looking for
Sheet1.Cells(lngRow, 1).Clear ' clear out the value in column A, is this is what is requried?
End If
Next lngRow
End Sub
It's basic stuff do to. VBA has basically the same syntax of VB 6.
You can read the language reference or hitting F1 on VBA editor for help.
Just for example, check this code. Besides your requirements are a bit confusing, pay attention in the code structure and the functions used, like Range.Cells, IsEmpty
Sub Macro1()
Dim limit, index As Integer
' Iterate limit, just for test
limit = 20
' Iterate your worksheet until the limit
For i = 1 To limit Step 1
' Range param is a string, you can do any string contatenation you wish
' Value property is what inside the cell
If (Not IsEmpty(Range("D" & i).Value) And IsEmpty(Range("A" & i).Value)) Then
' Requirement 2
Range("E" & i).Value = False
ElseIf (Range("A" & i).Value = "X") Then
' Requiriment 1
Range("E" & i).Value = True
' Requiriment 3
Range("A" & i).Value = Empty
End If
Next
End Sub
Like Siddharth said, recording a macro and study the generated code is a great way to learn some VBA tricks.

Paste data from one workbook to another based on searching column for "C" or "D"

I have run into a roadblock. I have a document that gets a status given to an audit item (circle, triangle, x). Currently, users have to manually write up the problem on another document. I want to auto populate this other document based on the selection in the cell.
In my example, cell string to review is V27:AD195. If any of these cells include "C" or "D" then it would return the value from Column "B" onto the PFUS Sample document's next available empty cell.
I am having trouble with my programming idea getting it to work...I don't want/need to copy the entire row just the cell in B column.
How do I upload the example?
My original programming idea is to use
Sub Sample()
Dim wb1 As Workbook, wb2 As Workbook
Dim ws1 As Worksheet, ws2 As Worksheet
Dim copyFrom As Range
Dim lRow As Long '<~~ Not Integer. Might give you error in higher versions of excel
Dim strSearch As String
Set wb1 = ThisWorkbook
Set ws1 = wb1.Worksheets("Test")
strSearch = "D"
With ws1
'~~> Remove any filters
.AutoFilterMode = False
'~~> I am assuming that the names are in Col A
'~~> if not then change A below to whatever column letter
lRow = .Range("E" & .Rows.Count).End(xlUp).Row
With .Range("E1:E" & lRow)
.AutoFilter Field:=1, Criteria1:="=*" & strSearch & "*"
Set copyFrom = .Offset(1, 0).SpecialCells(xlCellTypeVisible).EntireRow
End With
'~~> Remove any filters
.AutoFilterMode = False
End With
'~~> Destination File
Set wb2 = Application.Workbooks.Open("C:\Sample.xlsx")
Set ws2 = wb2.Worksheets("Sheet1")
With ws2
If Application.WorksheetFunction.CountA(.Cells) <> 0 Then
lRow = .Cells.Find(What:="*", _
After:=.Range("A1"), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Row
Else
lRow = 1
End If
copyFrom.Copy .Rows(lRow)
End With
wb2.Save
wb2.Close
End Sub
But I am having trouble manipulating this to work for my specific need because it copies the entire row. Any ideas? Beginner VBA here with limited knowledge but can pick up quick.
teylyn may have been a little abrupt but his complaint is correct. You are making it very difficult for anyone to help you by posting a large chunk of code that does not appear to relate to your question. If you had located the piece of code that was not doing what you wanted/expected and created a small example based on that faulty code you would probably have received an answer within minutes.
Issues with your code
"In my example, cell string to review is V27:AD195." By "cell string" do you mean "cell range"? Your code performs an AutoFilter on column E. How does this relate to V27:AD195?
You say you want either "C" or "D" but you only search for "D".
I rarely use AutoFilter and am not an expert. To me, this seems an unusual way to search a multi-column range for multiple values. You search for =*D*. My understanding is you can either search for a particular string, or blanks or non-blanks. I do not think there is a wild card facility which I assume is the purpose of the asterisks. I do not know why the equals is there.
If you are only interested in column B, why Set copyFrom = ... EntireRow?
You have two separate requirements. (1) Identify rows containing either "C" or "D". (2) Move the value of column B of each of those rows to another worksheet. You do not check that you have successfully achieved requirement 1 before attempting to achieve requirement 2.
My solution to requirement 1
There are many answers on StackOverflow that show how to move values from one worksheet to another so I have ignored requirement 2.
Requirement 1 is trickier and I have not seen a similar question.
I do not believe there is any sensible way of achieving requirement 1 with AutoFilter. If anyone knows different, I will be interested in knowing how since it means I have misunderstood the full capabilities of AutoFilter.
I could have used VBA to search the cell values but I believe Find, which will search for a string within a cell, will be faster. I have not tested this but the general advice is not to write VBA to duplicate Excel functionality. There is a Find All available from the keyboard but there is no VBA equivalent. However, I do not believe a VBA Find All would be helpful in this case.
The first thing I want to say about the code below is that it is full of Debug.Print statements. I did not write this code in one go. I stepped through the code and used the Debug.Print statements to check that each section did what I wanted before moving onto the next section. Also there are a lot of Debug.Assert False all but one of which has been commented out. When I start, I place a Debug.Assert False statement at the head of every path through the code. When one of these statements is reached, execution stops. I step on one statement and then comment out the Debug.Assert False. If there are any Debug.Assert False statement still active when I have finished, either I have not adequately tested my code or my design is faulty and the code cannot be reached. Either way, I have more work to do. There are other ways of achieving the same objectives but these techniques work for me.
Your code will have to look for "C" and then look for "D" and then merge the results. It is easier to use an array of search values in such cases than to duplicate the code so I have:
SearchValue = Array("C", "D", "Z", "G")
You only want "C" and "D" but I wanted to properly test my code. There are no "Z"s amid my test data so this array allowed me to test that the complete absence of a value is handled correctly.
I have two other arrays (RowFirst and RowNext) which I size to match SearchValue.
My test data is:
1 H I J K L M G
2 H I J D L M N
3 A B C K E F
4 H I J K L M N
5 O P Q R S T U
6 V W X Y X ABCDEF ABC
7 DEF AD A B E F G
8 H CAB ABD DEF L M N
9 C I J K L M N
10 H I J K L M N
11 H I D K L M N
12 H G J K L M N
13 H I G K L M N
14 H I J D L M N
15 H I J K L M N
16 H I J D L M N
The first significant block of code, searches for the first occurrences of the four values and store values to give:
SearchValue "C" "D" "Z" "G"
RowFirst 3 2 0 1
RowNext 3 2 0 1
The code uses Find repeatedly and it will eventually loop. When, in the the main loop, Find tell me it has found "C" on row 3 (the value in RowFirst), I know it has looped and every occurrence of "C" has been found and processed. RowNext = 0 in the "Z" column tells the code not to look for "Z"
The main loop first processes the match just found. The lowest value in RowNext is 1 so that is the next (first) row with one of these values. I record 1 in array RowMatch.
The code then updates RowNext for the next rows containing the search values after row 1. For "C" and "D", the next rows have already been found. There is to be no search for "Z". The next "G" is on row 7. So the arrays become:
SearchValue "C" "D" "Z" "G"
RowFirst 3 2 0 1
RowNext 3 2 0 7
When a Find loops, the RowNext for the value is set to 0 to indicate that value is finished. The main loop continues until all the RowNext values are 0.
For my test data, the rows with matching values (as stored in RowMatch) are:
1 2 3 6 7 8 9 11 12 13 14 16
If your data matched mine and if you were interested in "G", these are the rows whose column B you would move to the new worksheet.
I hope the above explanation, the comments in the code and the output from the Debug.Print statements are sufficient for you to understand the following code:
Option Explicit
Sub FindMatchingRows()
Dim ColRightToSearch As Long
Dim InxValueCrnt As Long
Dim InxMatchCrnt As Long
Dim InxMatchMax As Long
Dim RngMatch As Range
Dim RowBotToSearch As Long
Dim RngToSearch As Range
Dim RowFirst() As Long
Dim RowMatch() As Long
Dim RowNext() As Long
Dim RowFirstCrnt As Long
Dim SearchValue() As Variant
Dim WshtToSearch As Worksheet
' Specify search values
SearchValue = Array("C", "D", "Z", "G")
' Define worksheet and range to search. Change to your values
Set WshtToSearch = Worksheets("Sheet1")
Set RngToSearch = WshtToSearch.Range("A1:Z50")
' ReDim Preserve is a slow statement so I do not want to use it more often than
' necessary. When I do not know how many values I will want to store in an array I
' start with as many entries as I think will be enough and only enlarge the array
' if I fill it.
ReDim RowMatch(1 To 100)
InxMatchMax = 0 ' No rows with any of the values found yet
' One entry for each entry on SearchValue
' Search always start after the specified "after" cell, continues to the end of the
' range, loops to beginning of the range and continues to the "after" cell.
' RowFirst() is used to detect Find looping and finding the first row again.
' RowNext() records the most recent search.
ReDim RowFirst(LBound(SearchValue) To UBound(SearchValue))
ReDim RowNext(LBound(SearchValue) To UBound(SearchValue))
' Identify bottom range and rightmost column of range to be searched.
' See below for the use made of these values
RowBotToSearch = RngToSearch.Row + RngToSearch.Rows.Count - 1
ColRightToSearch = RngToSearch.Column + RngToSearch.Columns.Count - 1
Debug.Print "Bottom right cell is ("; RowBotToSearch & ", " & ColRightToSearch & ")"
' Initialise RowFirst and RowNext with the first row, if any, containing each
' search value. Each search must start after the bottom right cell of the search
' range so the search starts in the first cell of the range
RowFirstCrnt = 0 ' The first row containing any of the values
For InxValueCrnt = LBound(SearchValue) To UBound(SearchValue)
Set RngMatch = RngToSearch.Find(What:=SearchValue(InxValueCrnt), _
After:=WshtToSearch.Cells(RowBotToSearch, ColRightToSearch), _
LookIn:=xlValues, LookAt:=xlPart, SearchOrder:=xlByRows, _
SearchDirection:=xlNext)
If RngMatch Is Nothing Then
' This value not found within range
'Debug.Assert False ' Not tested
Debug.Print SearchValue(InxValueCrnt) & " not found within range"
RowNext(InxValueCrnt) = 0
Else
' This value found within range
'Debug.Assert False ' Not tested
Debug.Print SearchValue(InxValueCrnt) & " found on row " & _
RngMatch.Row & " in column " & RngMatch.Column
RowNext(InxValueCrnt) = RngMatch.Row ' First row containing this value
RowFirst(InxValueCrnt) = RngMatch.Row
If RowFirstCrnt = 0 Then
' First value found so first row found with matching value
'Debug.Assert False ' Not tested
RowFirstCrnt = RngMatch.Row
ElseIf RowFirstCrnt > RngMatch.Row Then
' This value found on earlier row than previous best
'Debug.Assert False ' Not tested
RowFirstCrnt = RngMatch.Row
End If
End If
Next
Debug.Print "First rows: ";
For InxValueCrnt = LBound(SearchValue) To UBound(SearchValue)
If RowFirst(InxValueCrnt) = 0 Then
'Debug.Assert False ' Not tested
Debug.Print " " & SearchValue(InxValueCrnt) & " not found ";
Else
'Debug.Assert False ' Not tested
Debug.Print " " & SearchValue(InxValueCrnt) & " on row "; RowFirst(InxValueCrnt) & " ";
End If
Next
Debug.Print
Do While RowFirstCrnt > 0
Debug.Print "Next row with a match is " & RowFirstCrnt
' Record this match
InxMatchMax = InxMatchMax + 1
If UBound(RowMatch) < InxMatchMax Then
'Debug.Assert False ' Not tested
ReDim Preserve RowMatch(1 To 100 + UBound(RowMatch))
End If
RowMatch(InxMatchMax) = RowFirstCrnt
' Now look for further matches
RowFirstCrnt = 0 ' NO match found so far
For InxValueCrnt = LBound(SearchValue) To UBound(SearchValue)
If RowNext(InxValueCrnt) = 0 Then
' Either this value was not found or all occurrences of this value
' have already been found and recorded
'Debug.Assert False ' Not tested
ElseIf RowNext(InxValueCrnt) > RowMatch(InxMatchMax) Then
' The next occurrence of this value is after the most recent matching
' row so this is still the next occurrence of this value
If RowFirstCrnt = 0 Then
' Could be next matching row
'Debug.Assert False ' Not tested
Debug.Print "First possible next match " & SearchValue(InxValueCrnt) & _
" on row " & RowNext(InxValueCrnt)
RowFirstCrnt = RowNext(InxValueCrnt)
ElseIf RowFirstCrnt > RowNext(InxValueCrnt) Then
' This value found on earlier row than previous best
'Debug.Assert False ' Not tested
Debug.Print "New next match " & SearchValue(InxValueCrnt) & _
" on row " & RowNext(InxValueCrnt)
RowFirstCrnt = RowNext(InxValueCrnt)
End If
Else
'Debug.Assert False ' Not tested
' Need to search again starting at the end of RowMatch(inxMatchMax)
' Note I cannot use FindNext because it continues the most recent
' and this code is performing different Finds
Set RngMatch = RngToSearch.Find(What:=SearchValue(InxValueCrnt), _
After:=WshtToSearch.Cells(RowMatch(InxMatchMax), ColRightToSearch), _
LookIn:=xlValues, LookAt:=xlPart, SearchOrder:=xlByRows, _
SearchDirection:=xlNext)
If RngMatch Is Nothing Then
' This should not be possible since we are only searching for value know to be present
Debug.Assert False ' Not tested
Else
'Debug.Assert False ' Not tested
Debug.Print SearchValue(InxValueCrnt) & " found on row " & _
RngMatch.Row & " in column " & RngMatch.Column
If RngMatch.Row = RowFirst(InxValueCrnt) Then
' Have looped back to first occurrence. All rows for this value
' found and recorded
'Debug.Assert False ' Not tested
RowNext(InxValueCrnt) = 0
Debug.Print SearchValue(InxValueCrnt) & " has looped"
Else
' New value found
'Debug.Assert False ' Not tested
RowNext(InxValueCrnt) = RngMatch.Row
If RowFirstCrnt = 0 Then
' First value found so first row found with matching value
'Debug.Assert False ' Not tested
RowFirstCrnt = RngMatch.Row
ElseIf RowFirstCrnt > RngMatch.Row Then
' This value found on earlier row than previous best
'Debug.Assert False ' Not tested
RowFirstCrnt = RngMatch.Row
End If
End If ' Process successful Find
End If ' Process result of Find
End If ' Decide if to search for this value
Next InxValueCrnt
Loop
Debug.Print "Rows with matching values:";
For InxMatchCrnt = 1 To InxMatchMax
Debug.Print " " & RowMatch(InxMatchCrnt);
Next
Debug.Print
End Sub

Compare and copy matching data from adjacent cells

I was having some trouble with a macro I have been writing. I am trying to find a match in column A and column D. When I detect a match I want to copy the adjacent cells of each I.E copy the contents of B of the line of the first match to E where the match occurs in D. Whenever I do this I never get the right copy. It will copy the values that match but put them in the completely wrong space. I only encounter a problem when the order is mixed up or there is a white space. Any suggestions would be helpful.
Thanks
Nick.
Note: In this version of my code I was using input boxes to pick what two columns of data the user wants to compare and the one he wants to copy from and paste too. It should not make a big difference.
Sub Copy()
Dim column1 As String
Dim column2 As String
Dim from As String
Dim too As String
numrows = Sheet1.Range("A1").Offset(Sheet1.Rows.Count - 1, 0).End(xlUp).Row
'MsgBox numrows
column1 = InputBox("which column do you want to select from")
column2 = InputBox("which column do you want to compare to ")
from = InputBox("which column do you want to copy data from")
too = InputBox("which column do you want to copy data to")
Dim lngLastRow As Long
Dim lngLoopCtr As Long
Dim i As Long
Dim j As Long
Dim value As String
lngLastRow = Range(column1 & Rows.Count).End(xlUp).Row
lngLastRow2 = Range(column2 & Rows.Count).End(xlUp).Row
'lngLastRow = Sheet1.Range("A1").Offset(Sheet1.Rows.Count - 1, 0).End(xlUp).Row
Dim temp As String
For i = 1 To lngLastRow Step 1
temp = Cells(i, column1).value
value = Cells(i, from).value
'MsgBox "temp"
'MsgBox (temp)
If Cells(i, column1).value <> "" Then
For j = 1 To lngLastRow2 Step 1
' MsgBox "cell"
' MsgBox (Cells(j, column2).value)
If Cells(j, column2).value = "" Then
Cells(j, column2).Offset(1, 0).Select
End If
If Cells(j, column2).value <> "" Then
If temp = Cells(j, column2).value Then
'MsgBox "equal"
'MsgBox "i"
'MsgBox i
'MsgBox "j"
'MsgBox j
'value = Cells(j, from).value
'MsgBox Cells(i, too).value
'Cells(i, too).value = Cells(j, from).value
'Dim num As Integer
'On Error Resume Next
'num = Application.WorksheetFunction.VLookup(temp, Sheet1.Range("A0:M13"), 3, False)
Cells(i, too).value = Cells(j, from).value
'MsgBox j
' MsgBox (Cells(i, column1).value)
' MsgBox "="
' MsgBox (Cells(j, column2).value)
End If
End If
Next j
End If
Next i
End Sub
I have studied your text and your macro and think the macro below does what you want.
If this macro does what you want, your problem was caused by your use of meaningless variable names such as: column1, column2, i and j. This meant you did not notice you were using the wrong variables in the statement that copied values.
I have renamed all your variables. I am not asking you to like my naming convention but I am recommending you have a naming convention. I can look at macros I wrote years ago and know what all the variables are because I developed my convention in my early days of VBA programming and have used it every since. This makes my life much easier when I need to update old macros.
I have added Option Explicit at the top of the module. Without this statement, a misspelt variable name becomes a declaration:
Dim Count As Long
Lots of statements
Count = Conut + 1
This causes Conut to be declared with a value of zero. Such errors can be a nightmare to find.
I have used a With Statement to make explicit which worksheet I am using.
You checked both cells to not be empty. I only check the first because it is not necessary to check the second since, if the second is empty, it will not match the first.
Your code did not stop working down the Compare column if it found a match so my code does the same. This is correct if values can repeat in the Compare column. If they cannot repeat, you may wish to add Exit For to exit the inner loop after a match has been processed.
I believe the above explains all the changes I hve made.
Option Explicit
Sub Copy()
Dim ColCompare As String
Dim ColCopyFrom As String
Dim ColCopyTo As String
Dim ColSelect As String
Dim RowCrntCompare As Long
Dim RowCrntSelect As Long
Dim RowLastColCompare As Long
Dim RowLastColSelect As Long
Dim SelectValue As String
With Sheet1
ColSelect = InputBox("which column do you want to select ColCopyFrom")
ColCompare = InputBox("which column do you want to compare to ")
ColCopyFrom = InputBox("which column do you want to copy data ColCopyFrom")
ColCopyTo = InputBox("which column do you want to copy data to")
RowLastColSelect = .Range(ColSelect & .Rows.Count).End(xlUp).Row
RowLastColCompare = .Range(ColCompare & .Rows.Count).End(xlUp).Row
For RowCrntSelect = 1 To RowLastColSelect Step 1
SelectValue = .Cells(RowCrntSelect, ColSelect).value
If SelectValue <> "" Then
For RowCrntCompare = 1 To RowLastColCompare Step 1
If SelectValue = Cells(RowCrntCompare, ColCompare).value Then
.Cells(RowCrntCompare, ColCopyTo).value = _
.Cells(RowCrntSelect, ColCopyFrom).value
End If
Next RowCrntCompare
End If
Next RowCrntSelect
End With
End Sub

Resizing Cell in excel macro

I'm trying to link data from an Excel sheet, copy them to another sheet, and then copy onto another workbook. The data is non-contiguous, and the amount of iterations I need is unknown.
A portion of the code that I have now is below:
Sub GetCells()
Dim i As Integer, x As Integer, c As Integer
Dim test As Boolean
x = 0
i = 0
test = False
Do Until test = True
Windows("Room Checksums.xls").Activate
'This block gets the room name
Sheets("Sheet1").Activate
Range("B6").Select
ActiveCell.Offset(i, 0).Select
Selection.Copy
Sheets("Sheet2").Activate
Range("A1").Activate
ActiveCell.Offset(x, 0).Select
ActiveSheet.Paste Link:=True
'This block gets the area
Sheets("Sheet1").Activate
Range("AN99").Select
ActiveCell.Offset(i, 0).Select
Selection.Copy
Sheets("Sheet2").Activate
Range("B1").Activate
ActiveCell.Offset(x, 0).Select
ActiveSheet.Paste Link:=True
i = i + 108
x = x + 1
Sheets("Sheet1").Activate
Range("B6").Activate
ActiveCell.Offset(i, 0).Select
test = ActiveCell.Value = ""
Loop
Sheets("Sheet2").Activate
ActiveSheet.Range(Cells(1, 1), Cells(x, 12)).Select
Application.CutCopyMode = False
Selection.Copy
Windows("GetReference.xlsm").Activate
Range("A8").Select
ActiveSheet.Paste Link:=True
End Sub
The problem is that it is copying and pasting each cell one by one, flipping between sheets in the process. What I'd like to do is select a number of scattered cells, offset by 108 cells, and select the next number of scattered cells (re-sizing).
What would be the best way to do so?
I have been studying the end result of your macro. My objective is to identify a better approach to achieving that result rather than tidying your existing approach.
You name your two workbooks: "Room Checksums.xls" and "GetReference.xlsm". "xls" is the extension of an Excel 2003 workbook. "xlsm" is the extension of a post-2003 workbook that contains macros. Perhaps you are using these extensions correctly but you should check.
I use Excel 2003 so all my workbooks have an extension of "xls". I suspect you will need to change this.
I have created three workbooks: "Room Checksums.xls", "GetReference.xls" and "Macros.xls". "Room Checksums.xls" and "GetReference.xls" contain nothing but data. The macros are in "Macros.xls". I use this division when only privileged users can run the macros and I do not wish ordinary users to be bothered by or have access to those macros. My macro below can be placed without changes within "GetReference.xls" if you prefer.
The image below shows worksheet “Sheet1” of "Room Checksums.xls". I have hidden most of the rows and columns because they contain nothing relevant to your macro. I have set the cell values to their addresses for my convenience but there is no other significance to these values.
I ran your macro. “Sheet2” of "Room Checksums.xls" became:
Note: the formula bar shows cell A1 as =Sheet1!$B$6. That is, this is a link not a value.
The active worksheet of "GetReference.xls” became:
Note 1: the zeros in columns C to L are because you move 12 columns. I assume there is other data in these columns of “Sheet2” of your "Room Checksums.xls" that you want.
Note 2: the formula bar shows cell A8 as ='[Room Checksums.xls]Sheet2'!A1.
My macro achieves the same result as yours but in a somewhat different manner. However, there are a number of features to my macro which I need to explain. They are not strictly necessary but I believe they represent good practice.
Your macro contains a lot of what I call magic numbers. For example: B6, AN99, 108 and A8. It is possible that these values are meaningful to your company but I suspect they are accidents of the current workbooks. You use the value 108 several times. If this value were to change to 109, you would have to search your code for 108 and replace it by 109. The number 108 is sufficiently unusual for it to be unlikely that it occurs in your code for other reasons but other numbers may not be so unusual making replacement a painstaking task. At the moment you may know what this number means. Will you remember when you return to amend this macro in 12 months?
I have defined 108 as a constant:
Const Offset1 As Long = 108
I would prefer a better name but I do not know what this number is. You could replace all occurrences of “Offset1” with a more meaningful name. Alternatively, you could add comments explaining what it is. If the value becomes 109, one change to this statement fixes the problem. I think most of my names should be replaced with something more meaningful.
You assume "Room Checksums.xls" and "GetReference.xlsm" are open. If one of both of them were not open, the macro would stop on the relevant activate statement. Perhaps an earlier macro has opened these workbooks but I have added code to check that they are open.
My macro does not paste anything. It has three phases:
Work down worksheet “Sheet1” of "Room Checksums.xls" to identify last non-empty cell in the sequence: B6, B114, B222, B330, B438, ... .
Create links to these entries (and the AN99 series) in worksheet “Sheet2” of "Room Checksums.xls". Formulae are just strings which start with the symbol “=” and they can be created like any other string.
Create links in worksheet “Xxxxxx” of "GetReference.xls” to the table in “Sheet2” of "Room Checksums.xls". I do not like relying on the correct worksheet being active. You will have to replace “Xxxxxx” with the correct value.
In my macro I have attempted to explain what I am doing but I have not said much about the syntax of the statements I am using. You should have little difficulty finding explanations of the syntax but do ask if necessary.
I think you will find some of my statements confusing. For example:
.Cells(RowSrc2Crnt, Col1Src2).Value = "=" & WshtSrc1Name & "!$" & Col1Src1 & _
"$" & Row1Src1Start + OffsetCrnt
None of the names are as meaningful as I would like because I do not understand the purpose of the worksheets, columns and offset. Instead of copying and pasting, I am building a formula such as “=Sheet1!$B$6”. If you work through the expression you should be able to relate each term with an element of the formula:
"=" =
WshtSrc1Name Sheet1
"!$" !$
Col1Src1 B
"$" $
Row1Src1Start + OffsetCrnt 6
This macro is not quite as I would have coded it for myself since I prefer to use arrays rather than access worksheets directly. I decided that I was introducing more than enough concepts without the addition of arrays.
Even without arrays this macro is more difficult for a newbie to understand than I had expected when I started coding it. It is divided into three separate phases each with a separate purpose which should help a little. If you study it, I hope you can see why it would be easier to maintain if the format of the workbooks changed. If you have large volumes of data, this macro would be substantially faster than yours.
Option Explicit
Const ColDestStart As Long = 1
Const Col1Src1 As String = "B"
Const Col2Src1 As String = "AN"
Const Col1Src2 As String = "A"
Const Col2Src2 As String = "B"
Const ColSrc2Start As Long = 1
Const ColSrc2End As Long = 12
Const Offset1 As Long = 108
Const RowDestStart As Long = 8
Const Row1Src1Start As Long = 6
Const Row2Src1Start As Long = 99
Const RowSrc2Start As Long = 1
Const WbookDestName As String = "GetReference.xls"
Const WbookSrcName As String = "Room Checksums.xls"
Const WshtDestName As String = "Xxxxxx"
Const WshtSrc1Name As String = "Sheet1"
Const WshtSrc2Name As String = "Sheet2"
Sub GetCellsRevised()
Dim ColDestCrnt As Long
Dim ColSrc2Crnt As Long
Dim InxEntryCrnt As Long
Dim InxEntryMax As Long
Dim InxWbookCrnt As Long
Dim OffsetCrnt As Long
Dim OffsetMax As Long
Dim RowDestCrnt As Long
Dim RowSrc2Crnt As Long
Dim WbookDest As Workbook
Dim WbookSrc As Workbook
' Check the source and destination workbooks are open and create references to them.
Set WbookDest = Nothing
Set WbookSrc = Nothing
For InxWbookCrnt = 1 To Workbooks.Count
If Workbooks(InxWbookCrnt).Name = WbookDestName Then
Set WbookDest = Workbooks(InxWbookCrnt)
ElseIf Workbooks(InxWbookCrnt).Name = WbookSrcName Then
Set WbookSrc = Workbooks(InxWbookCrnt)
End If
Next
If WbookDest Is Nothing Then
Call MsgBox("I need workbook """ & WbookDestName & """ to be open", vbOKOnly)
Exit Sub
End If
If WbookSrc Is Nothing Then
Call MsgBox("I need workbook """ & WbookSrcName & """ to be open", vbOKOnly)
Exit Sub
End If
' Phase 1. Locate the last non-empty cell in the sequence: B6, B114, B222, ...
' within source worksheet 1
OffsetCrnt = 0
With WbookSrc.Worksheets(WshtSrc1Name)
Do While True
If .Cells(Row1Src1Start + OffsetCrnt, Col1Src1).Value = "" Then
Exit Do
End If
OffsetCrnt = OffsetCrnt + Offset1
Loop
End With
If OffsetCrnt = 0 Then
Call MsgBox("There is no data to reference", vbOKOnly)
Exit Sub
End If
OffsetMax = OffsetCrnt - Offset1
' Phase 2. Build table in source worksheet 2
RowSrc2Crnt = RowSrc2Start
With WbookSrc.Worksheets(WshtSrc2Name)
For OffsetCrnt = 0 To OffsetMax Step Offset1
.Cells(RowSrc2Crnt, Col1Src2).Value = "=" & WshtSrc1Name & "!$" & Col1Src1 & _
"$" & Row1Src1Start + OffsetCrnt
.Cells(RowSrc2Crnt, Col2Src2).Value = "=" & WshtSrc1Name & "!$" & Col2Src1 & _
"$" & Row2Src1Start + OffsetCrnt
RowSrc2Crnt = RowSrc2Crnt + 1
Next
End With
' Phase 3. Build table in destination worksheet
RowSrc2Crnt = RowSrc2Start
RowDestCrnt = RowDestStart
With WbookDest.Worksheets(WshtDestName)
For OffsetCrnt = 0 To OffsetMax Step Offset1
ColDestCrnt = ColDestStart
For ColSrc2Crnt = ColSrc2Start To ColSrc2End
.Cells(RowDestCrnt, ColDestCrnt).Value = _
"='[" & WbookSrcName & "]" & WshtSrc2Name & "'!" & _
ColNumToCode(ColSrc2Crnt) & RowSrc2Crnt
ColDestCrnt = ColDestCrnt + 1
Next
RowSrc2Crnt = RowSrc2Crnt + 1
RowDestCrnt = RowDestCrnt + 1
Next
End With
End Sub
Function ColNumToCode(ByVal ColNum As Long) As String
Dim Code As String
Dim PartNum As Long
' Last updated 3 Feb 12. Adapted to handle three character codes.
If ColNum = 0 Then
ColNumToCode = "0"
Else
Code = ""
Do While ColNum > 0
PartNum = (ColNum - 1) Mod 26
Code = Chr(65 + PartNum) & Code
ColNum = (ColNum - PartNum - 1) \ 26
Loop
End If
ColNumToCode = Code
End Function