Excel macro: Combine rows if column match - vba

I want to be able to combine the rows for which the value in the first column matches, so that the values of non-blank cells are consolidated into one row. E.g.:
Mary Smith, A, [blank cell]
Mary Smith, [blank cell], B
-->
Mary Smith A B
I've tried to use the code below:
Dim RowNum As Long, LastRow As Long
Application.ScreenUpdating = False
RowNum = 4
LastRow = Cells.SpecialCells(xlCellTypeLastCell).Row
Range("A4", Cells(LastRow, 13)).Select
For Each Row In Selection
With Cells
If Cells(RowNum, 1) = Cells(RowNum + 1, 1) Then
Cells(RowNum + 1, 1).Copy Destination:=Cells(RowNum, 1)
Cells(RowNum + 1, 2).Copy Destination:=Cells(RowNum, 2)
Cells(RowNum + 1, 3).Copy Destination:=Cells(RowNum, 3)
Cells(RowNum + 1, 4).Copy Destination:=Cells(RowNum, 4)
Cells(RowNum + 1, 5).Copy Destination:=Cells(RowNum, 5)
Cells(RowNum + 1, 6).Copy Destination:=Cells(RowNum, 6)
Cells(RowNum + 1, 7).Copy Destination:=Cells(RowNum, 7)
Cells(RowNum + 1, 8).Copy Destination:=Cells(RowNum, 8)
Cells(RowNum + 1, 9).Copy Destination:=Cells(RowNum, 9)
Cells(RowNum + 1, 10).Copy Destination:=Cells(RowNum, 10)
Cells(RowNum + 1, 11).Copy Destination:=Cells(RowNum, 11)
Cells(RowNum + 1, 12).Copy Destination:=Cells(RowNum, 12)
Cells(RowNum + 1, 13).Copy Destination:=Cells(RowNum, 13)
Rows(RowNum + 1).EntireRow.Delete
End If
End With
RowNum = RowNum + 1
Next Row
Application.ScreenUpdating = True
'
End Sub
This does a fine job of consolidating the data so that there are only unique values in the first column, HOWEVER, when the row is copied up, the values of blank cells copy over populated cells, which NOT what I want. So for instance, running this macro on the above data would yield:
Mary Smith, A, [blank cell]
Mary Smith, [blank cell], B
-->
Mary Smith, A, [blank cell]
Any insight into how I might modify the above code (or use something more elegant) would be appreciated!!

This will do it very quickly:
Sub foo()
Dim ws As Worksheet
Dim lstrow As Long
Set ws = Sheets("Sheet1") ' Change to your sheet
With ws
lstrow = .Range("A" & .Rows.Count).End(xlUp).Row
With .Range("B4:M" & lstrow)
.Offset(, 26).FormulaR1C1 = "=IFERROR(INDEX(R4C[-26]:R" & lstrow & "C[-26],MATCH(1,INDEX((R4C1:R" & lstrow & "C1 = RC1)*(R4C[-26]:R" & lstrow & "C[-26] <>""""),),0)),"""")"
ws.Calculate
.Value = .Offset(, 26).Value
.Offset(, 26).ClearContents
End With
With .Range("A4:M" & lstrow)
.Value = .Value
.RemoveDuplicates 1, xlGuess
End With
End With
End Sub
It basically uses the formula: =INDEX(B$4:B$4,MATCH(1,INDEX(($A$4:$A$4 = $A4)*(B$4:B$4 <>""),),0)) To find all the values. Puts those formulas in blank columns and then copies the data back and removes the duplicates.
This will do all 13 columns at once.
It also does not care how many times the value in Column A is repeated. There could be 4 Mary Smiths in that column. It will grab the first value in each column and use that.
Before:
After:

Try the below code
Sub test()
LastRow = Range("A" & Rows.Count).End(xlUp).Row
For i = 4 To LastRow
If ((Range("A" & i).Value = Range("A" & i + 1).Value) And (Range("B" & i).Value <> Range("B" & i + 1).Value) And ((Range("B" & i).Value = "") Or (Range("B" & i + 1).Value = "")) And (Range("C" & i).Value <> Range("C" & i + 1).Value) And ((Range("C" & i).Value = "") Or (Range("C" & i + 1).Value = ""))) Then
If Range("B" & i).Value = "" Then
Range("B" & i).Value = Range("B" & i + 1).Value
ElseIf Range("B" & i + 1).Value = "" Then
Range("B" & i + 1).Value = Range("B" & i).Value
End If
If Range("C" & i).Value = "" Then
Range("C" & i).Value = Range("C" & i + 1).Value
ElseIf Range("C" & i + 1).Value = "" Then
Range("C" & i + 1).Value = Range("C" & i).Value
End If
End If
Range("B" & i).EntireRow.Delete Shift:=(xlUp)
LastRow = LastRow - 1
Next i
End Sub

Here is another approach.
Create a Personnel object. Each Personnel object can have multiple attributes (the non blank column entries in your original table).
By using the Key property of the collection object, and using the Name (column1 data) as the key, we can detect duplicates without having to sort the original data. And the number of attributes for each name is limited only by the size of the worksheet.
Other information is in the comments.
Insert a class object and rename it cPersonnel
Below is the code for the Class and Regular modules
Class Module
Option Explicit
Private pName As String
Private pAttrib As String
Private pAttribs As Collection
Public Property Get Name() As String
Name = pName
End Property
Public Property Let Name(Value As String)
pName = Value
End Property
Public Property Get Attrib() As String
Attrib = pAttrib
End Property
Public Property Let Attrib(Value As String)
pAttrib = Value
End Property
Public Property Get AttribS() As Collection
Set AttribS = pAttribs
End Property
Public Function ADDAttribS(Value As String)
pAttribs.Add Value
End Function
Private Sub Class_Initialize()
Set pAttribs = New Collection
End Sub
Regular Module
Option Explicit
Sub PersonnelAttribs()
Dim wsSrc As Worksheet, wsRes As Worksheet, rRes As Range
Dim vSrc As Variant, vRes() As Variant
Dim cP As cPersonnel, colP As Collection
Dim LastRow As Long, LastCol As Long
Dim I As Long, J As Long
'Set source and results worksheets, ranges
Set wsSrc = Worksheets("sheet1")
Set wsRes = Worksheets("sheet2")
Set rRes = wsRes.Cells(1, 1)
With wsSrc.Cells
LastRow = .Find(what:="*", after:=.Cells(1, 1), LookIn:=xlFormulas, _
searchorder:=xlByRows, searchdirection:=xlPrevious).Row
LastCol = .Find(what:="*", after:=.Cells(1, 1), LookIn:=xlFormulas, _
searchorder:=xlByColumns, searchdirection:=xlPrevious).Column
End With
'Read source data into array
With wsSrc
vSrc = Range(.Cells(1, 1), .Cells(LastRow, LastCol))
End With
'create and collect the Personnel objects
'Source data does not need to be sorted
Set colP = New Collection
On Error Resume Next
For I = 1 To UBound(vSrc, 1)
If Trim(vSrc(I, 1)) <> "" Then
Set cP = New cPersonnel
With cP
.Name = vSrc(I, 1)
For J = 2 To UBound(vSrc, 2)
If Trim(vSrc(I, J)) <> "" Then
.Attrib = Trim(vSrc(I, J))
.ADDAttribS .Attrib
End If
Next J
colP.Add cP, .Name
Select Case Err.Number
Case 457 'duplicate name
Err.Clear
For J = 1 To .AttribS.Count
colP(.Name).ADDAttribS .AttribS(J)
Next J
Case Is <> 0
Debug.Print Err.Number, Err.Description
Stop
End Select
End With
End If
Next I
On Error GoTo 0
'Create results array
'Number of columns
For I = 1 To colP.Count
With colP(I)
J = IIf(J > .AttribS.Count, J, .AttribS.Count)
End With
Next I
ReDim vRes(0 To colP.Count, 0 To J)
'Headers
vRes(0, 0) = "Name"
For J = 1 To UBound(vRes, 2)
vRes(0, J) = "Attrib " & J
Next J
'Populate data
For I = 1 To colP.Count
With colP(I)
vRes(I, 0) = .Name
For J = 1 To .AttribS.Count
vRes(I, J) = .AttribS(J)
Next J
End With
Next I
'Clear old data and write new
Set rRes = rRes.Resize(UBound(vRes, 1) + 1, UBound(vRes, 2) + 1)
With rRes
.EntireColumn.Clear
.Value = vRes
With .Rows(1)
.Font.Bold = True
.HorizontalAlignment = xlCenter
End With
.EntireColumn.AutoFit
End With
End Sub
Original Data
Results after Macro

Related

Looping and Copy Data to a new sheet

This is my first post, but I have an excel workbook with multiple tabs formatted like this:
I'm learning vba but don't know the excel functions well enough yet to loop through the rows and copy the data reformatted to a new sheet into this format:
Specifically I think i need to
- Initialize the range of worksheets
loop through them
store column headers for b through G into a variable so when the x is found it can be concatenated or copied over
handle the blank spaces so that the last value is used for each one
Any help appreciated. Thanks!
The following will do what you expect:
Sub foo()
Dim ws As Worksheet: Set ws = Sheets("Sheet1")
'declare and set your worksheet, amend as required
Dim wsResult As Worksheet: Set wsResult = Sheets("Sheet2")
LastRow = ws.Cells(ws.Rows.Count, "J").End(xlUp).Row
'get the last row with data on Column J
For i = 2 To LastRow
NextRow = wsResult.Cells(wsResult.Rows.Count, "D").End(xlUp).Row + 1
ws.Range("H" & i & ":J" & i).Copy Destination:=wsResult.Range("D" & NextRow)
wsResult.Range("A" & NextRow).Value = "Title"
If ws.Cells(i, 1) <> "" Then 'if GroupID is not empty
wsResult.Range("B" & NextRow).Value = ws.Cells(i, 1) 'grab that GroupID
Else
x = i
Do While Trim(ws.Cells(x, 1)) = ""
x = x - 1
Group = ws.Cells(x, 1) 'get the GroupID of the Row above
Loop
wsResult.Range("B" & NextRow).Value = Group
End If
For y = 2 To 7
If ws.Cells(i, y) <> "" Then
Level = ws.Cells(1, y).Value
Exit For
End If
Next y
wsResult.Cells(NextRow, 3) = Level
Next i
End Sub
Try this:
Sub BuildTable()
Dim data(), i As Integer, j As Integer
data = Worksheets("Sheet1").Range("A2:J5").Value
For i = 1 To UBound(data)
With Worksheets("Sheet2")
.Range("A" & i) = "Title"
.Range("D" & i) = data(i, 8)
.Range("E" & i) = data(i, 9)
.Range("F" & i) = data(i, 10)
If i > 1 Then
.Range("B" & i) = IIf(data(i, 1) <> "", data(i, 1), .Range("B" & i - 1))
Else
.Range("B" & i) = data(i, 1)
End If
For j = 2 To 7
If data(i, j) = "x" Then
.Range("C" & i) = "Level" & j - 1
End If
Next j
End With
Next i
End Sub
You can use Power Query Excel 2010+ you can download and activate easily or by default in 2016 version. There you can transform your data and put it as a table or a pivot table:

select each cell from a column and loop through a column in another workbook if it exists Excel VBA Macro

I have 2 workbooks called "Source1" and "Source2".
For each cell in the last column of "Source1" I check if it exists in the last column of "Source2".
If yes, then I copy 4 separate cells from that row based on some critea into a new workbook called "Target".
My macro is working but as I have thousands of cells to loop through, it takes me at least 10 min till the macro finishes. I am running it many times a day so I want to optimize my code so that it will take less time.
Here is my code
Sub Loop_Cells()
Application.ScreenUpdating = False
Application.DisplayAlerts = False
Application.SheetsInNewWorkbook = 1
Dim Source, Source2, Target As Workbook
Dim c As Range
Dim lRow, lRow2 As Long
Dim x, y, w As Integer
Set Source = Workbooks.Open("C:\Reports\Source1.xlsx")
Source.Activate
x = ActiveSheet.UsedRange.Columns.Count
ActiveSheet.Cells(1, x + 1) = "Concate"
lRow = ActiveSheet.Range("A" & Rows.Count).End(xlUp).Row
For i = 2 To lRow
ActiveSheet.Cells(i, x + 1).Value = ActiveSheet.Cells(i, 6).Value & ActiveSheet.Cells(i, 7).Value
Next i
ActiveSheet.Columns(x + 1).NumberFormat = "0"
Set Source2 = Workbooks.Open("C:\Reports\Source2.xlsx")
Source2.Activate
y = ActiveSheet.UsedRange.Columns.Count
ActiveSheet.Cells(1, y + 1) = "Concate"
lRow2 = ActiveSheet.Range("A" & Rows.Count).End(xlUp).Row
For i = 2 To lRow2
ActiveSheet.Cells(i, y + 1).Value = ActiveSheet.Cells(i, 48).Value & ActiveSheet.Cells(i, 3).Value
Next i
ActiveSheet.Columns(y + 1).NumberFormat = "0"
Set Target = Workbooks.Add
Target.Sheets(1).Name = "ExistCells"
Source.Sheets(1).Activate
w = 1
For Each c In Source1.Sheets(1).UsedRange.Columns(x + 1).Cells
For j = 2 To lRow2
If c.Value = Source2.Sheets(1).Cells(j, y + 1).Value Then
Target.Sheets(1).Cells(w, 1).Value = Source2.Sheets(1).Cells(j, 48).Value
Target.Sheets(1).Cells(w, 2).Value = Source2.Sheets(1).Cells(j, 3).Value
Target.Sheets(1).Cells(w, 3).Value = Source2.Sheets(1).Cells(j, 27).Value
Target.Sheets(1).Cells(w, 4).Value = Source2.Sheets(1).Cells(j, 41).Value
w = w + 1
End If
Next j
Next c
Workbooks("Source1.xlsx").Close SaveChanges:=False
Workbooks("Source1.xlsx").Close SaveChanges:=False
Target.Activate
ActiveWorkbook.SaveAs FileName:= "C:\Reports\Target.xlsx", _
FileFormat:=xlOpenXMLWorkbook, CreateBackup:=False
Application.ScreenUpdating = True
Application.DisplayAlerts = True
End Sub
I think the problem is in this part, when the cell exists I don't need to loop till the last row and I should move to the next.
For j = 2 To lRow2
If c.Value = Source2.Sheets(1).Cells(j, y + 1).Value Then ...
Any Suggestions how to adjust my code?
Collections: VBA.Collection, Scripting.Dictionary, ArrayList, Queue, Stack ... etc.
Collections are optimized for fast lookups. For this reason,they are ideal when matching values.
Consider matching two lists each with 1000 values. Assuming that on average you find a match half way through the list, that's (500 * 1000) or 500K operations. Using a Collection would reduce the number to 1000 iterations + 1000 lookups. Assuming that it takes 1 to 10 operations per lookup (just a guess) then you would reduce the number of operations that it takes to compare two 1000 element lists from 500K to 6K.
Arrays: Reading and writing to arrays is much faster then reading and writing to file (worksheet).
Once a match is found you write 4 values to the new worksheet. Let's say you find 1000 matches, that's 4000 write operations to the worksheet. If instaed you hold these values in an array and then write the array to the worksheet you'll reduce the number of write operations (to the worksheet) from 400 to 1.
Using these techniques should reduce the run time from 10+ minutes to under 20 seconds.
Sub NewLoop()
Application.ScreenUpdating = False
Application.SheetsInNewWorkbook = 1
Dim data As Variant, result As Variant
Dim lastRow As Long, x As Long, x1 As Long
Dim key As String
Dim list As Object
Set list = CreateObject("System.Collections.ArrayList")
With Workbooks.Open("C:\Reports\Source1.xlsx")
With .Worksheets(1)
data = .Range("F2:G" & .Range("A" & Rows.Count).End(xlUp).Row).Value
For x = 1 To UBound(data, 1)
'Create a Unique Identifier using a pipe to delimit the data
'This will keep the data from mixing
key = data(x, 1) & "|" & data(x, 2)
If Not list.Contains(key) Then list.Add key
Next
End With
.Close SaveChanges:=False
End With
With Workbooks.Open("C:\Reports\Source2.xlsx")
With .Worksheets(1)
lastRow = .Range("A" & Rows.Count).End(xlUp).Row
ReDim result(1 To lastRow, 1 To 4)
For x = 2 To lastRow
'Create a Unique Identifier using a pipe to delimit the data
'This will keep the data from mixing
key = .Cells(i, 48).Value & "|" & .Cells(i, 3).Value
If list.Contains(key) Then
x1 = x1 + 1
result(x1, 1) = .Cells(j, 48).Value
result(x1, 2) = .Cells(j, 3).Value
result(x1, 3) = .Cells(j, 27).Value
result(x1, 4) = .Cells(j, 41).Value
End If
Next
End With
.Close SaveChanges:=False
End With
With Workbooks.Add
With Worksheets(1)
.Name = "ExistCells"
.Range("A1:D1").Resize(x1).Value = Results
End With
End With
Application.ScreenUpdating = True
End Sub
Following on from your last point, could you not just exit the loop when the If condition is met? Something like this for example?
For j = 2 To lRow2
If c.Value = Source2.Sheets(1).Cells(j, y + 1).Value Then
Target.Sheets(1).Cells(w, 1).Value = Source2.Sheets(1).Cells(j, 48).Value
Target.Sheets(1).Cells(w, 2).Value = Source2.Sheets(1).Cells(j, 3).Value
Target.Sheets(1).Cells(w, 3).Value = Source2.Sheets(1).Cells(j, 27).Value
Target.Sheets(1).Cells(w, 4).Value = Source2.Sheets(1).Cells(j, 41).Value
w = w + 1
GoTo ExitLoop
End If
Next j
ExitLoop:
The code could be cleaned up a bit...plus you were closing "Source1.xlsx" twice...and tried to refer to Source1 as a variable even though it was never declared. Using Option Explicit at the top of the module will allow you find that type of issue easily. I put in a similar break in the inner For loop like Wilson88 as well.
By using your variables and With you should be able to speed it up some over ActiveWorkbook and ActiveSheet...
Sub Loop_Cells()
Dim Source As Workbook, Source2 As Workbook, Target As Workbook
Dim w As Integer, x As Integer, y As Integer
Dim lRow As Long, lRow2 As Long
Dim c As Range
Application.ScreenUpdating = False
Application.DisplayAlerts = False
Application.SheetsInNewWorkbook = 1
Set Source = Workbooks.Open("C:\Reports\Source1.xlsx")
With Source
x = .UsedRange.Columns.Count
.Cells(1, x + 1) = "Concate"
lRow = .Range("A" & Rows.Count).End(xlUp).Row
For i = 2 To lRow
.Cells(i, x + 1) = .Cells(i, 6). & .Cells(i, 7)
Next i
.Columns(x + 1).NumberFormat = "0"
End With
Set Source2 = Workbooks.Open("C:\Reports\Source2.xlsx")
With Source2
y = .UsedRange.Columns.Count
.Cells(1, y + 1) = "Concate"
lRow2 = .Range("A" & Rows.Count).End(xlUp).Row
For i = 2 To lRow2
.Cells(i, y + 1). = .Cells(i, 48) & .Cells(i, 3)
Next i
.Columns(y + 1).NumberFormat = "0"
End With
Set Target = Workbooks.Add
With Target.Sheets(1)
.Name = "ExistCells"
w = 1
For Each c In Source.Sheets(1).UsedRange.Columns(x + 1).Cells
For j = 2 To lRow2
If c.Value = Source2.Sheets(1).Cells(j, y + 1) Then
.Cells(w, 1).Value = Source2.Sheets(1).Cells(j, 48)
.Cells(w, 2).Value = Source2.Sheets(1).Cells(j, 3)
.Cells(w, 3).Value = Source2.Sheets(1).Cells(j, 27)
.Cells(w, 4).Value = Source2.Sheets(1).Cells(j, 41)
w = w + 1
Exit For
End If
Next j
Next c
End With
Source.Close SaveChanges:=False
Source2.Close SaveChanges:=False
Target.SaveAs FileName:= "C:\Reports\Target.xlsx", _
FileFormat:=xlOpenXMLWorkbook, CreateBackup:=False
Application.ScreenUpdating = True
Application.DisplayAlerts = True
End Sub

VBA code cannot run in shared workbook

Just a concern regarding shared workbooks. I have a script that moves a certain row to the appropriate sheet based on cell values.
When I copy the row, the format is usually pasted in unshared workbooks.
However, in shared workbooks, formats are completely ignored.
I can't seem the find the reason why....
Any help would be greatly appreciated.
Thanks
Sub RunScriptButton_Click()
'On Error GoTo CleanFail
If MsgBox("Run Script?", vbYesNo, "Run Script") = vbNo Then
Exit Sub
End If
'Disables screen flashing when the information is updated
Application.ScreenUpdating = False
Dim project As String, ws As Worksheet, ignoredSheets As Object, scheduleSheets As Object
Dim legendSht As Worksheet, masterSht As Worksheet
Dim i As Integer, j As Integer, k As Integer, x As Integer, y As Integer, z As Integer
Dim lastrow As Integer, lastcoln As Integer, lastrow2 As Integer, lastrow3 As Integer, lastRowLegend As Integer
Dim rowht As Double, rowht2 As Double
Dim count As Integer, SAcount As Integer
Dim ID As String, name As Range, allppl As Range, allppl2 As Range
Dim month_col As Range, month_col_no As Integer, next_month_col As Range, next_month As Integer
Dim mcount1 As Integer, mcount2 As Integer, first As Integer, secnd As Integer
Dim monthrow As Integer, script_info_row As Integer, proj_coln As Integer, name_coln As Integer, assist_coln As Integer
Set legendSht = ThisWorkbook.Worksheets("Legend")
Set masterSht = ThisWorkbook.Worksheets("Master Schedule")
'----------------------------------------------------------
' Set the worksheet names to be ignored by the script (non-schedule sheets)
' Add additional exceptions by adding a new item to the dictionary with "Sheet Name", [next number]
Set ignoredSheets = CreateObject("Scripting.Dictionary")
ignoredSheets.Add "Legend", 1
ignoredSheets.Add "Master Schedule", 2
ignoredSheets.Add "Surveyor Overview", 3
'----------------------------------------------------------
lastRowLegend = legendSht.UsedRange.Row - 1 + legendSht.UsedRange.Rows.count
script_info_row = legendSht.Range(legendSht.Cells(1, 1), legendSht.Cells(lastRowLegend, 1)).Find(what:="Script Information").Row + 1
With masterSht
'Find last row with data on the master schedule sheet
Set tempRange = .Cells(.Rows.count, "B").End(xlUp)
lastrow = tempRange.Row
'Find last column with data on the master schedule sheet
If .Cells(2, .Columns.count) <> vbNullString Then
Set tempRange = .Cells(2, .Columns.count)
lastcoln = tempRange.Column
Else
Set tempRange = .Cells(2, .Columns.count).End(xlToLeft)
lastcoln = tempRange.Column
End If
proj_coln = .Range(.Cells(1, 1), .Cells(1, lastcoln)).Find(what:="Project").Column
name_coln = .Range(.Cells(1, 1), .Cells(1, lastcoln)).Find(what:="Name").Column
assist_coln = .Range(.Cells(1, 1), .Cells(1, lastcoln)).Find(what:="Assistant").Column
'startCol = .Range(.Cells(1, 1), .Cells(1, lastcoln)).Find(what:=CStr(lastUpdateDate + 1)).Column
'endCol = .Range(.Cells(1, 1), .Cells(1, lastcoln)).Find(what:=CStr(currentDate)).Column
End With
Set scheduleSheets = CreateObject("Scripting.Dictionary")
'Loops through each worksheet except for legend and master schedule worksheet and deletes all information
For Each ws In ThisWorkbook.Worksheets
If Not ignoredSheets.Exists(ws.name) Then
ws.Cells.Delete
'Repositions buttons that get shoved off the page?
'For Each Control In ws.Shapes
' If Control.Type = msoOLEControlObject Then
' Control.Top = 48
' Control.Left = 9.75
' End If
'Next Control
'MsgBox Mid(ws.name, InStr(ws.name, "(") + 1, InStr(ws.name, ")") - InStr(ws.name, "(") - 1)
scheduleSheets.Add Mid(ws.name, InStr(ws.name, "(") + 1, InStr(ws.name, ")") - InStr(ws.name, "(") - 1), ws.Index
End If
Next ws
'copies the headers and dates from master schedule sheet
With masterSht
.Range(.Cells(1, 1), .Cells(2, lastcoln)).Copy
rowht = .Rows(1).RowHeight
rowht2 = .Rows(2).RowHeight
End With
'pastes the copied headers into every sheet except for ignored sheets
For Each ws In ThisWorkbook.Worksheets
If Not ignoredSheets.Exists(ws.name) Then
With ws
.Range("A1").PasteSpecial xlPasteColumnWidths
.Range("A1").PasteSpecial xlPasteFormats
.Range("A1").PasteSpecial xlPasteValuesAndNumberFormats
.Rows(1).RowHeight = rowht
.Rows(2).RowHeight = rowht2
End With
End If
Next ws
'Checks number in Project column of Master Schedule and copies row into sheet with matching number between brackets in sheet name
For i = 3 To lastrow
project = masterSht.Cells(i, proj_coln)
'Loop through stored sheet project numbers and compare to current row to find the correct sheet to copy to
For Each strKey In scheduleSheets.Keys()
If InStr(project, strKey) <> 0 Then
masterSht.Range(masterSht.Cells(i, 1), masterSht.Cells(i, lastcoln)).Copy
ThisWorkbook.Sheets(scheduleSheets.Item(strKey)).Range("A" & i).PasteSpecial xlPasteColumnWidths
ThisWorkbook.Sheets(scheduleSheets.Item(strKey)).Range("A" & i).PasteSpecial xlPasteFormats
ThisWorkbook.Sheets(scheduleSheets.Item(strKey)).Range("A" & i).PasteSpecial xlPasteValuesAndNumberFormats
ThisWorkbook.Sheets(scheduleSheets.Item(strKey)).Range("A" & i).PasteSpecial xlPasteComments
'If only one project number in this item, then break out of looping through sheet names and go to next row in schedule
If InStr(project, "/") = 0 Then
Exit For
End If
End If
Next
Next i
'Deletes empty rows in sheets other than legend and master schedule
For Each ws In ThisWorkbook.Worksheets
If Not ignoredSheets.Exists(ws.name) Then
ws.Cells.EntireColumn.Hidden = False
With ws.UsedRange
For j = .Rows.count To 3 Step -1
If Application.WorksheetFunction.CountA(.Rows(j).EntireRow) = 0 Then
.Rows(j).EntireRow.Delete
End If
Next j
End With
lastrow = ws.UsedRange.Rows.count
'Count the number of survey assistants in each project worksheet
SAcount = Application.WorksheetFunction.CountIfs(ws.Range(ws.Cells(3, name_coln), ws.Cells(lastrow, name_coln)), "SA:*")
'Crew count labels
ws.Range("A" & lastrow + 1) = "Total Crew Count: " & lastrow - 2 - SAcount
ws.Range("E" & lastrow + 2) = "Double Crew Count"
ws.Range("E" & lastrow + 3) = "Single Crew Count"
'Get total crew count by counting number of party chiefs (hide SAs)
Set allppl = ws.Range(ws.Cells(3, name_coln), ws.Cells(lastrow, name_coln))
For Each name In allppl
If Left(name, 3) = "SA:" Then
name.EntireRow.Hidden = True
End If
Next name
'Tally active crews for each day
For j = assist_coln To lastcoln
'Find 3 letter code for current project sheet
ID = Application.WorksheetFunction.Index(Sheet1.Range(Sheet1.Cells(script_info_row, 1), Sheet1.Cells(lastRowLegend, 3)), _
Application.WorksheetFunction.Match(ws.name, Sheet1.Range(Sheet1.Cells(script_info_row, 1), Sheet1.Cells(lastRowLegend, 1)), 0), 2)
'Count number of active crews for the current day
count = COUNTIFv(ws.Range(ws.Cells(2, j), ws.Cells(lastrow, j)), "*" & ID & "*")
ws.Cells(lastrow + 1, j).Value = count
Next j
'Unhide all cells
ws.Cells.EntireRow.Hidden = False
'Hide all crew except survey assistants to determine number of 2-man crews
If lastrow - 2 - SAcount > 0 Then
For Each name In allppl
If Left(name, 3) <> "SA:" Then
name.EntireRow.Hidden = True
End If
Next name
End If
'Tally active 2-man crews for each day
For j = assist_coln To lastcoln
'ID = Application.WorksheetFunction.Index(Sheet1.Range(Sheet1.Cells(script_info_row, 1), Sheet1.Cells(lastRowLegend, 3)), _
'Application.WorksheetFunction.Match(ws.name, Sheet1.Range(Sheet1.Cells(script_info_row, 1), Sheet1.Cells(lastRowLegend, 1)), 0), 2)
count2 = COUNTIFv(ws.Range(ws.Cells(2, j), ws.Cells(lastrow, j)), "*" & ID & "*")
ws.Cells(lastrow + 2, j).Value = count2 'Active two-man crews for current date
ws.Cells(lastrow + 3, j).Value = ws.Cells(lastrow + 1, j) - count2 'One-man crew = Total crew - 2M crew
Next j
ws.Cells.EntireRow.Hidden = False
'Hide all schedule columns prior to current day
month_col_no = ws.Range(ws.Cells(2, 1), ws.Cells(2, lastcoln)).Find(what:=Format(Now, "m/d/yyyy")).Column
ws.Range(ws.Cells(1, assist_coln), ws.Cells(1, month_col_no - 1)).EntireColumn.Hidden = True
ws.Activate
ActiveWindow.ScrollRow = 1
'Tabulate monthly crew counts
lastrow3 = ws.UsedRange.Rows.count
monthrow = lastrow3 + 1
For i = Month(Date) To 12
month_col_no = ws.Range(ws.Cells(2, 1), ws.Cells(2, lastcoln)).Find(what:=i & "/1/" & Year(Date)).Column
If i <> 12 Then
next_month = ws.Range(ws.Cells(2, 1), ws.Cells(2, lastcoln)).Find(what:=i + 1 & "/1/" & Year(Date)).Column
Else
next_month = ws.Range(ws.Cells(2, 1), ws.Cells(2, lastcoln)).Find(what:="12/31/" & Year(Date)).Column + 1
End If
mcount1 = Application.Sum(ws.Range(ws.Cells(lastrow3 - 1, month_col_no), ws.Cells(lastrow3 - 1, next_month - 1)))
mcount2 = Application.Sum(ws.Range(ws.Cells(lastrow3, month_col_no), ws.Cells(lastrow3, next_month - 1)))
ws.Cells(monthrow, 1) = MonthName(i) & " Double Crew Total: " & mcount1
ws.Cells(monthrow + 1, 1) = MonthName(i) & " Single Crew Total: " & mcount2
monthrow = monthrow + 2
Next i
End If
Next ws
With masterSht
.Activate
ActiveWindow.ScrollRow = 1
month_col_no = .Range(.Cells(2, 1), .Cells(2, lastcoln)).Find(what:=Month(Date) & "/" & Day(Date) & "/" & Year(Date)).Column
.Range(.Cells(1, assist_coln + 1), .Cells(1, month_col_no - 1)).EntireColumn.Hidden = True
End With
'enables screen flash and auto calculation again
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic
'CleanExit:
'Cleanup code
MsgBox "Process complete"
' Exit Sub
'CleanFail:
' Raise Err.Number
' Resume CleanExit
' Resume
End Sub
Shared workbooks have limitations. The biggest one is that they can become corrupt at any time and are impossible to troubleshoot because their behaviour is not consistent.
Avoid shared workbooks.

Have search function need help editing

So I have a code I have written the first part of the code is to create a new worksheet with the headings specified. The second part of the code is meant to populate that table with certain information. The problem I am having is getting the correct bits of information to go into the correct columns.
I need the code to search for the value 9.1 in column G in all worksheets within a workbook
if that value is found I need it to copy this to column b in the new sheet along with the following information :
Engine Effect from Column F Same row must be pasted to Column C in the worksheet entitled FHA
Part number is always located in Cell J3 this must be pasted into column D and is always the same
Part Name Is Always located in C2 this must be pasted into column E and is always the same
FM ID from Column B same row must be pasted to Column F in the worksheet entitled FHA
Failure Mode & Cause from Column C Same row must be pasted to column G in FHA
FMCN Value From Column N pasted to Column H In FHA
As It stands the code I have is
Sub createWSheetFHA()
Worksheets.Add(After:=Worksheets(Worksheets.Count)).Name = "FHA"
Cells(1, 2) = "FHA TABLE"
Cells(2, 2) = "FHA Ref"
Cells(2, 3) = "Engine Effect"
Cells(2, 4) = "Part No"
Cells(2, 5) = "Part Name"
Cells(2, 6) = "FM I.D"
Cells(2, 7) = "Failure Mode & Cause"
Cells(2, 8) = "FMCM"
Cells(2, 9) = "PTR"
Cells(2, 10) = "ETR"
Range(Cells(2, 2), Cells(2, 10)).Font.Bold = True
Range(Cells(1, 2), Cells(1, 10)).MergeCells = True
Range(Cells(1, 2), Cells(1, 10)).Font.Bold = True
End Sub
Sub Populate_FHA_Table_2()
Dim wks As Excel.Worksheet, i As Integer, n As Integer
Application.ScreenUpdating = False
Sheets("FHA").Range("A2:" & Columns.Count & ":" & Rows.Count).Delete
i = 1
For Each wks In ActiveWorkbook.Worksheets
If wks.Name <> "FHA" Then
wks.UsedRange.AutoFilter Field:=7, Criteria1:="9.1"
Sheets(i).Range(Sheets(i).Range("G1").Offset(1), Sheets(i).Range("B1").End(xlDown)).Copy _
Sheets("FHA").Range("C" & Rows.Count).End(xlUp)
Sheets(i).Range(Sheets(i).Range("F1").Offset(1), Sheets(i).Range("D1").End(xlDown)).Copy _
Sheets("FHA").Range("d" & Rows.Count).End(xlUp)
Sheets(i).Range(Sheets(i).Range("J1").Offset(1), Sheets(i).Range("E1").End(xlDown)).Copy _
Sheets("FHA").Range("e" & Rows.Count).End(xlUp)
Sheets(i).Range(Sheets(i).Range("C1").Offset(1), Sheets(i).Range("H1").End(xlDown)).Copy _
Sheets("FHA").Range("E" & Rows.Count).End(xlUp)
Sheets(i).Range(Sheets(i).Range("B1").Offset(1), Sheets(i).Range("H1").End(xlDown)).Copy _
Sheets("FHA").Range("F" & Rows.Count).End(xlUp)
Sheets(i).Range(Sheets(i).Range("C1").Offset(1), Sheets(i).Range("H1").End(xlDown)).Copy _
Sheets("FHA").Range("G" & Rows.Count).End(xlUp)
Sheets(i).Range(Sheets(i).Range("N1").Offset(1), Sheets(i).Range("H1").End(xlDown)).Copy _
Sheets("FHA").Range("H" & Rows.Count).End(xlUp)
wks.UsedRange.AutoFilter
End If
i = i + 1
Next
Application.ScreenUpdating = True
End Sub
You have some mismatches in your code (Example using 'for each wk' then accessing via an index 'i'; where they may not necessarily match)
Try something like this...
I have added in some dynamic flow control which isn't strictly needed but if and when your headers change in the future, it may be easier to have it in this form.
Likewise I have tried to add in some error handling as well
Sub Create_FHA_Sheet()
Dim Headers() As String: Headers = _
Split("FHA Ref,Engine Effect,Part No,Part Name,FM I.D,Failure Mode & Cause,FMCM,PTR,ETR", ",")
If Not WorksheetExists("FHA") Then Worksheets.Add().Name = "FHA"
Dim wsFHA As Worksheet: Set wsFHA = Sheets("FHA")
wsFHA.Move after:=Worksheets(Worksheets.Count)
wsFHA.Cells.Clear
Application.ScreenUpdating = False
With wsFHA
For i = 0 To UBound(Headers)
.Cells(2, i + 2) = Headers(i)
.Columns(i + 2).EntireColumn.AutoFit
Next i
.Cells(1, 2) = "FHA TABLE"
.Range(.Cells(1, 2), .Cells(1, UBound(Headers) + 2)).MergeCells = True
.Range(.Cells(1, 2), .Cells(1, UBound(Headers) + 2)).HorizontalAlignment = xlCenter
.Range(.Cells(1, 2), .Cells(2, UBound(Headers) + 2)).Font.Bold = True
End With
Dim RowCounter As Long: RowCounter = 3
Dim SearchTarget As String: SearchTarget = "9.1"
Dim SourceCell As Range, FirstAdr As String
If Worksheets.Count > 1 Then
For i = 1 To Worksheets.Count - 1
With Sheets(i)
Set SourceCell = .Columns(7).Find(SearchTarget, LookAt:=xlWhole)
If Not SourceCell Is Nothing Then
FirstAdr = SourceCell.Address
Do
wsFHA.Cells(RowCounter, 3).Value = .Cells(SourceCell.Row, 6).Value
wsFHA.Cells(RowCounter, 4).Value = .Cells(3, 10).Value
wsFHA.Cells(RowCounter, 5).Value = .Cells(2, 3).Value
wsFHA.Cells(RowCounter, 6).Value = .Cells(SourceCell.Row, 2).Value
wsFHA.Cells(RowCounter, 7).Value = .Cells(SourceCell.Row, 3).Value
wsFHA.Cells(RowCounter, 8).Value = .Cells(SourceCell.Row, 14).Value
Set SourceCell = .Columns(7).FindNext(SourceCell)
RowCounter = RowCounter + 1
Loop While Not SourceCell Is Nothing And SourceCell.Address <> FirstAdr
End If
End With
Next i
End If
Application.ScreenUpdating = True
End Sub
Public Function WorksheetExists(ByVal WorksheetName As String) As Boolean
On Error Resume Next
WorksheetExists = (ThisWorkbook.Sheets(WorksheetName).Name <> "")
On Error GoTo 0
End Function

Excel VBA - Combine rows with duplicate values in one cell and merge values in other cell

I am trying to find duplicate values in one column and combine the values of a second column into one row. I also want to sum the values in a third column.
For example:
A B C D
h 4 w 3
h 4 u 5
h 4 g 7
h 4 f 4
k 9 t 6
k 9 o 6
k 9 p 9
k 9 j 1
Would become
A B C D
k 9 t;o;p;j 22
h 4 w;u;g;f 19
The code I have been using for the first part of this is
Sub mergeCategoryValues()
Dim lngRow As Long
With ActiveSheet
lngRow = .Cells(65536, 1).End(xlUp).Row
.Cells(1).CurrentRegion.Sort key1:=.Cells(1), Header:=xlYes
Do
If .Cells(lngRow, 9) = .Cells(lngRow + 1, 9) Then
.Cells(lngRow, 11) = .Cells(lngRow, 8) & "; " & .Cells(lngRow + 1, 8)
.Rows(lngRow +1).Delete
End If
lngRow = lngRow - 1
Loop Until lngRow < 2
End With
End Sub
(please forgive the indentation)
The problem that I am running into is that it will find the first pair of duplicates, but not all. So I get a result that looks like this:
A B C D
k 9 t;o 12
k 9 p;j 10
h 4 w;u 8
h 4 g;f 11
Thoughts?
Thank you in advance.
Try changing your code to this:
Sub mergeCategoryValues()
Dim lngRow As Long
With ActiveSheet
lngRow = .Cells(65536, 1).End(xlUp).Row
.Cells(1).CurrentRegion.Sort key1:=.Cells(1), Header:=xlYes
Do
If .Cells(lngRow, 1) = .Cells(lngRow - 1, 1) Then
.Cells(lngRow - 1, 3) = .Cells(lngRow - 1, 3) & "; " & .Cells(lngRow, 3)
.Cells(lngRow - 1, 4) = .Cells(lngRow - 1, 4) + .Cells(lngRow, 4)
.Rows(lngRow).Delete
End If
lngRow = lngRow - 1
Loop Until lngRow = 1
End With
End Sub
Tested
EDIT
To make it a little easier to adjust to different column I added variables at the beginning to indicate which column do what. Note that column 2 (B) isn't used in the current logic.
Sub mergeCategoryValues()
Dim lngRow As Long
With ActiveSheet
Dim columnToMatch As Integer: columnToMatch = 1
Dim columnToConcatenate As Integer: columnToConcatenate = 3
Dim columnToSum As Integer: columnToSum = 4
lngRow = .Cells(65536, columnToMatch).End(xlUp).Row
.Cells(columnToMatch).CurrentRegion.Sort key1:=.Cells(columnToMatch), Header:=xlYes
Do
If .Cells(lngRow, columnToMatch) = .Cells(lngRow - 1, columnToMatch) Then
.Cells(lngRow - 1, columnToConcatenate) = .Cells(lngRow - 1, columnToConcatenate) & "; " & .Cells(lngRow, columnToConcatenate)
.Cells(lngRow - 1, columnToSum) = .Cells(lngRow - 1, columnToSum) + .Cells(lngRow, columnToSum)
.Rows(lngRow).Delete
End If
lngRow = lngRow - 1
Loop Until lngRow = 1
End With
End Sub
This looks sloppy and complicated. Both are true, but it works pretty fine.
Note! I always recommend to define all DIMs like: ranges, integers, etc. Storing the last row to a variable like LngRow is best (not like the whole App.WksFunc.COUNTA). I also like to use functions directly on cells where possible (like the SUMIFS ex. below). Thus, based on your example configuration (columns ABCD):
Sub Test_Texas2014()
Dim MySheet As Worksheet: Set MySheet = Sheets("Sheet1")
'Clear the previous results before populating
MySheet.Range("F:I").Clear
'Step1 Find distinct values on column A and copy them on F
For i = 1 To Application.WorksheetFunction.CountA(MySheet.Range("A:A"))
Row_PasteCount = Application.WorksheetFunction.CountA(MySheet.Range("F:F")) + 1
Set LookupID = MySheet.Range("A" & i)
Set LookupID_SearchRange = MySheet.Range("F:F")
Set CopyValueID_Paste = MySheet.Range("F" & Row_PasteCount)
If IsError(Application.Match(LookupID, LookupID_SearchRange, 0)) Then
LookupID.Copy
CopyValueID_Paste.PasteSpecial xlPasteValues
End If
Next i
'Step2 fill your values in columns G H I based on selection
For j = 1 To Application.WorksheetFunction.CountA(MySheet.Range("F:F"))
Set ID = MySheet.Range("F" & j)
Set Index = MySheet.Range("G" & j)
Set AttributeX = MySheet.Range("H" & j)
Set SumX = MySheet.Range("I" & j)
For k = 1 To Application.WorksheetFunction.CountA(MySheet.Range("A:A"))
Set SearchedID = MySheet.Range("A" & k)
Set SearchedID_Index = MySheet.Range("B" & k)
Set SearchedID_AttributeX = MySheet.Range("C" & k)
Set SearchedID_SumX = MySheet.Range("D" & k)
If ID.Value = SearchedID.Value Then
Index.Value = SearchedID_Index.Value
AttributeX.Value = AttributeX.Value & ";" & SearchedID_AttributeX.Value
SumX.Value = SumX.Value + SearchedID_SumX.Value
End If
Next k
Next j
End Sub
'Although for the sum I would use something like:
MySheet.Range("I1").Formula = "=SUMIFS(D:D,A:A,F1)"
MySheet.Range("I1").Copy
MySheet.Range("I2:I" & Application.WorksheetFunction.CountA(MySheet.Range("I:I"))).pasteSpecial xlPasteFormulas
'Similar for the Index with a Vlookup or Index(Match())
Merging rows by summing the numbers from column D and building a string concatenation from column C with a semi-colon delimiter based upon duplicate values in columns A and B.
Before¹:
        
Code:
Sub merge_A_to_D_data()
Dim rw As Long, lr As Long, str As String, dbl As Double
Application.ScreenUpdating = False
With ActiveSheet.Cells(1, 1).CurrentRegion
.Cells.Sort Key1:=.Columns(1), Order1:=xlAscending, _
Key2:=.Columns(2), Order2:=xlAscending, _
Orientation:=xlTopToBottom, Header:=xlYes
lr = .Rows.Count
For rw = .Rows.Count To 2 Step -1
If .Cells(rw, 1).Value2 <> .Cells(rw - 1, 1).Value2 And _
.Cells(rw, 2).Value2 <> .Cells(rw - 1, 2).Value2 And rw < lr Then
.Cells(rw, 4) = Application.Sum(.Range(.Cells(rw, 4), .Cells(lr, 4)))
.Cells(rw, 3) = Join(Application.Transpose(.Range(.Cells(rw, 3), .Cells(lr, 3))), Chr(59))
.Cells(rw + 1, 1).Resize(lr - rw, 1).EntireRow.Delete
lr = rw - 1
End If
Next rw
End With
Application.ScreenUpdating = True
End Sub
After¹:
        
¹Some additional rows of data were added to the original posted data in order to demonstrate the sort.
Here is my solution
Sub MyCombine()
Dim i As Integer
ActiveSheet.Sort.SortFields.Add Key:=Range("A:A"), _
SortOn:=xlSortOnValues, Order:=xlAscending, DataOption:=xlSortNormal
With ActiveSheet.Sort
.SetRange Range("A:D")
.Header = xlYes
.MatchCase = False
.Orientation = xlTopToBottom
.SortMethod = xlStroke
.Apply
End With
i = 2
Do Until Len(Cells(i, 1).Value) = 0
If Cells(i, 1).Value = Cells(i + 1, 1).Value Then
Cells(i, 3).Value = Cells(i, 3).Value & ";" & Cells(i + 1, 3).Value
Cells(i, 4).Value = Cells(i, 4).Value + Cells(i + 1, 4).Value
Rows(i + 1).Delete
Else
i = i + 1
End If
Loop
End Sub
.Cells(lngRow, 11) = .Cells(lngRow, 8) & "; " & .Cells(lngRow + 1, 8)
should be
.Cells(lngRow, 11) = .Cells(lngRow, 8) & "; " & .Cells(lngRow + 1, 11)
This will do what you want.
Sub Macro()
Dim lngRow As Long
For lngRow = ActiveSheet.Cells(Rows.Count, "A").End(xlUp).Row To 2 Step -1
If StrComp(Range("B" & lngRow), Range("B" & lngRow - 1), vbTextCompare) = 0 Then
If Range("C" & lngRow) <> "" Then
Range("C" & lngRow - 1) = Range("C" & lngRow - 1) & ";" & Range("C" & lngRow)
Range("D" & lngRow - 1) = Range("D" & lngRow - 1) + Range("D" & lngRow)
End If
Rows(lngRow).Delete
End If
Next
End Sub