Excel VBA to reorder rows based on 2 variables - vba

I am trying to order, from most senior downwards, a list of employees based on who their manager is. The tricky part is having all senior persons employees listed before moving on to the next person of the same level. For example see this image:
All people reporting to Dick, either directly or indirectly, are listed below him before moving on to Peter - the next person at his level.
So if the table looked like this:
Is there a VBA that will reorder the table to look like the first example? The table to be reordered may not look like this - it would have to work regardless of who it was wrongly ordered.
Note: The order of people at the same level is unimportant...
Thanks for your help.

I love stuff like this. Create an Excel sheet with two tabs, and name one "Input" and one "Output". Copy your example table into the "Input" then just the headers into "Output". Then plug the code below in. This should show you the idea, which is basic recursion. It's fairly horrible to do in VBA, it would be a lot prettier in C.
Option Explicit
Dim RawName() As Variant
Dim RawManager() As Variant
Dim RawLevel() As Variant
Dim TopNode As Integer
Sub FncSortHierarchy()
TopNode = FncPopulateRawHierarchy("A", "B", "C")
If TopNode <> 0 Then
Sheets("Output").Select
FncWritePerson (TopNode)
FncGetSubordinates (TopNode)
End If
End Sub
Private Function FncGetSubordinates(indexManager As Integer) As Integer
Dim i As Integer
Dim name As String
Dim manager As String
manager = RawName(indexManager)
For i = 1 To UBound(RawName)
If RawManager(i) = manager Then
name = RawName(i)
FncWritePerson (i)
FncGetSubordinates (i)
End If
Next i
End Function
Private Function FncWritePerson(index As Integer)
Dim nextRow As Integer
nextRow = ActiveSheet.UsedRange.Rows.Count + 1
Range("A" & nextRow) = RawName(index)
Range("B" & nextRow) = RawManager(index)
Range("C" & nextRow) = RawLevel(index)
End Function
Private Function FncPopulateRawHierarchy(nameCol As String, managerCol As String, levelCol As String) As Integer
Dim i As Integer
Sheets("Input").Select
ReDim RawName(ActiveSheet.UsedRange.Rows.Count)
ReDim RawManager(ActiveSheet.UsedRange.Rows.Count)
ReDim RawLevel(ActiveSheet.UsedRange.Rows.Count)
For i = 2 To ActiveSheet.UsedRange.Rows.Count
RawName(i - 1) = Range(nameCol & i).Value
RawManager(i - 1) = Range(managerCol & i).Value
RawLevel(i - 1) = Range(levelCol & i).Value
If RawManager(i - 1) = "N/A" Then FncPopulateRawHierarchy = i - 1
Next i
End Function

Related

COUNTIF() in 'For' loop

I have a column with nearly 100k and am trying to determine how many times a value occurs repeatedly in that column. I can do it row by row currently, but this is menial as a programmer, through something like =COUNTIF(D:D,D2). Yet that only returns D2 matches in column D.
I need to iterate through all values of D returning countif, therefore revealing all of the values repetitions in the column. I can remove duplicates later! So I have a dev. button a basic sub, or function (man this is new to me) and something along the lines of the most basic for loop ever. Just getting caught up on how to implement the COUNTIF() to to the loop properly.
Right now I'm looking at:
Sub doloop()
Dim i As Integer
i = 1
Do While i < D.Length
Cells(i, 8).Value =CountIf(D:D,D[i])
i = i + 1
Loop
End Sub
That code is incorrect obviously but it is where I'm at and may help for anyone more familiar with other languages.
Use Application.WorksheetFunction.CountIf() in your loop.
Private Sub doloop()
Dim lastRow As Long
Dim d As Double
Dim r As Range
Dim WS As Excel.Worksheet
Dim strValue As String
Dim lRow As Long
'Build your worksheet object
Set WS = ActiveWorkbook.Sheets("sheet1")
'Get the last used row in column A
lastRow = WS.Cells(WS.Rows.count, "D").End(xlUp).Row
'Build your range object to be searched
Set r = WS.Range("D1:D" & lastRow)
lRow = 1
WS.Activate
'Loop through the rows and do the search
Do While lRow <= lastRow
'First, get the value we will search for from the current row
strValue = WS.Range("D" & lRow).Value
'Return the count from the CountIf() worksheet function
d = Application.worksheetFunction.CountIf(r, strValue)
'Write that value to the current row
WS.Range("H" & lRow).Value = d
lRow = lRow + 1
Loop
End Sub
I believe you are trying to write the value to the cell, that is what the above does. FYI, if you want to put a formula into the cell, here is how that is done. Use this in place of WS.Range("H" & lRow).Value = d
WS.Range("H" & lRow).Formula = "=CountIf(D:D, D" & lRow & ")"
Sounds like you may want to look into using tables in Excel and capitalizing on their features like filtering and equation autofill. You may also be interested in using a PivotTable to do something very similar to what you're describing.
If you really want to go about this the programmatic way, I think the solution Matt gives answers your question about how to do this using CountIf. There's a big detriment to using CountIf though, in that it's not very computationally efficient. I don't think the code Matt posted will really be practical for processing the 100K rows mentioned in the OP (Application.ScreenUpdating = false would help some). Here's an alternative method that's a lot more efficient, but less intuitive, so you'll have to decide what suites your needs and what you feel conformable with.
Sub CountOccurances()
'Define Input and Output Ranges
'The best way to do this may very from case to case,
'So it should be addressed seperately
'Right now we'll assume current sheet rows 1-100K as OP specifies
Dim RInput, ROutput As Range
Set RInput = Range("D1:D100000")
Set ROutput = Range("E1:E100000")
'Define array for housing and processing range values
Dim A() As Variant
ReDim A(1 To RInput.Rows.Count, 0)
'Use Value2 as quicker more accurate value
A = RInput.Value2
'Create dictionary object
Set d = CreateObject("Scripting.Dictionary")
'Loop through array, adding new values and counting values as you go
For i = 1 To UBound(A)
If d.Exists(A(i, 1)) Then
d(A(i, 1)) = d(A(i, 1)) + 1
Else
d.Add A(i, 1), 1
End If
Next
'Overwrite original array values with count of that value
For i = 1 To UBound(A)
A(i, 1) = d(A(i, 1))
Next
'Write resulting array to output range
ROutput = A
End Sub
You can also modify this to include the removal of replicates you mentioned.
Sub CountOccurances_PrintOnce()
'Define Input and Output Ranges
'The best way to do this may very from case to case,
'So it should be addressed seperately
'Right now we'll assume current sheet rows 1-100K as OP specifies
Dim RInput, ROutput As Range
Set RInput = Range("D1:D100000")
Set ROutput = Range("F1:F9")
'Define array for housing and processing range values
Dim A() As Variant
ReDim A(1 To RInput.Rows.Count, 0)
'Use Value2 as quicker more accurate value
A = RInput.Value2
'Create dictionary object
Set d = CreateObject("Scripting.Dictionary")
'Loop through array, adding new values and counting values as you go
For i = 1 To UBound(A)
If d.Exists(A(i, 1)) Then
d(A(i, 1)) = d(A(i, 1)) + 1
Else
d.Add A(i, 1), 1
End If
Next
'Print results to VBA's immediate window
Dim sum As Double
For Each K In d.Keys
Debug.Print K & ": " & d(K)
sum = sum + d(K)
Next
Debug.Print "Total: " & sum
End Sub

Collect unique identifiers from one column and paste the results in a different worksheet.

What I'm looking to do is comb through a column and pull all the unique identifiers out of that column and then paste the results in a table in a different worksheet. I found the code below and it is very close to what I need. However, I have two major problems with it that I cannot figure out. First the area that this macro searches is constant ie "A1:B50". I need this to be one column and be dynamic since more data and new unique identifiers will be added to this worksheet. Second I cannot figure out how to paste my results to a specific range on a different worksheet. For example if I wanted to take the results and paste them in "sheet2" starting in at "B5" and going to however long the list of unique identifiers is.
Sub ExtractUniqueEntries()
Const ProductSheetName = "Sheet1" ' change as appropriate
Const ProductRange = "B2:B"
Const ResultsCol = "E"
Dim productWS As Worksheet
Dim uniqueList() As String
Dim productsList As Range
Dim anyProduct
Dim LC As Integer
ReDim uniqueList(1 To 1)
Set productWS = Worksheets(ProductSheetName)
Set productsList = productWS.Range(ProductRange)
Application.ScreenUpdating = False
For Each anyProduct In productsList
If Not IsEmpty(anyProduct) Then
If Trim(anyProduct) <> "" Then
For LC = LBound(uniqueList) To UBound(uniqueList)
If Trim(anyProduct) = uniqueList(LC) Then
Exit For ' found match, exit
End If
Next
If LC > UBound(uniqueList) Then
'new item, add it
uniqueList(UBound(uniqueList)) = Trim(anyProduct)
'make room for another
ReDim Preserve uniqueList(1 To UBound(uniqueList) + 1)
End If
End If
End If
Next ' end anyProduct loop
If UBound(uniqueList) > 1 Then
'remove empty element
ReDim Preserve uniqueList(1 To UBound(uniqueList) - 1)
End If
'clear out any previous entries in results column
If productWS.Range(ResultsCol & Rows.Count).End(xlUp).Row > 1 Then
productWS.Range(ResultsCol & 2 & ":" & _
productWS.Range(ResultsCol & Rows.Count).Address).ClearContents
End If
'list the unique items found
For LC = LBound(uniqueList) To UBound(uniqueList)
productWS.Range(ResultsCol & Rows.Count).End(xlUp).Offset(1, 0) = _
uniqueList(LC)
Next
'housekeeping cleanup
Set productsList = Nothing
Set productWS = Nothing
End Sub
I think your solution is a bit more tricky than it needs to be. Collecting unique ids becomes almost trivial is you use a Dictionary instead of a list. The added benefit is that a dictionary will scale much better than a list as your data set becomes larger.
The code below should provide you with a good starting point to get you going. For convenience's sake I used the reference from your post. So output will be on sheet2 to starting in cell B5 going down and the input is assumed to be on sheet1 cell B2 going down.
If you have any questions, please let me know.
Option Explicit
Sub ExtractUniqueEntries()
'enable microsoft scripting runtime --> tools - references
Dim unique_ids As New Dictionary
Dim cursor As Range: Set cursor = ThisWorkbook.Sheets("Sheet1").Range("B2") 'change as Required
'collect the unique ids
'This assumes that:
'1. ids do not contain blank rows.
'2. ids are properly formatted. Should this not be the could you'll need to do some validating.
While Not IsEmpty(cursor)
unique_ids(cursor.Value) = ""
Set cursor = cursor.Offset(RowOffset:=1)
Wend
'output the ids to some target.
'assumes the output area is blank.
Dim target As Range: Set target = ThisWorkbook.Sheets("Sheet2").Range("B5")
Dim id_ As Variant
For Each id_ In unique_ids
target = id_
Set target = target.Offset(RowOffset:=1)
Next id_
End Sub
A small modification will do it; the key is to define the ProductRange.
Sub ExtractUniqueEntries()
Const ProductSheetName = "Sheet1" ' change as appropriate
Dim ProductRange
ProductRange = "B2:B" & Range("B" & Cells.Rows.Count).End(xlUp).Row
Const ResultsCol = "E"
Dim productWS As Worksheet
Dim uniqueList() As String
Dim productsList As Range
Dim anyProduct
Dim LC As Integer
ReDim uniqueList(1 To 1)
Set productWS = Worksheets(ProductSheetName)
Set productsList = productWS.Range(ProductRange)
Application.ScreenUpdating = False
For Each anyProduct In productsList
If Not IsEmpty(anyProduct) Then
If Trim(anyProduct) <> "" Then
For LC = LBound(uniqueList) To UBound(uniqueList)
If Trim(anyProduct) = uniqueList(LC) Then
Exit For ' found match, exit
End If
Next
If LC > UBound(uniqueList) Then
'new item, add it
uniqueList(UBound(uniqueList)) = Trim(anyProduct)
'make room for another
ReDim Preserve uniqueList(1 To UBound(uniqueList) + 1)
End If
End If
End If
Next ' end anyProduct loop
If UBound(uniqueList) > 1 Then
'remove empty element
ReDim Preserve uniqueList(1 To UBound(uniqueList) - 1)
End If
'clear out any previous entries in results column
If productWS.Range(ResultsCol & Rows.Count).End(xlUp).Row > 1 Then
productWS.Range(ResultsCol & 2 & ":" & _
productWS.Range(ResultsCol & Rows.Count).Address).ClearContents
End If
'list the unique items found
For LC = LBound(uniqueList) To UBound(uniqueList)
productWS.Range(ResultsCol & Rows.Count).End(xlUp).Offset(1, 0) = _
uniqueList(LC)
Next
'housekeeping cleanup
Set productsList = Nothing
Set productWS = Nothing
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

Vlookup Function will not find correct values

I have a simple index file which will cycle through 500+ files and will retrieve relevant information. One of the index fields is a VLookUp, which references another sheet in the active index Workbook. While testing the script, the VLookUp function cannot find the appropriate value, even though it will when I manually enter the same formula into a cell. Any ideas why this might be happening (it returns "N/A" every time, as indicated by the ErrorHandling):
Public Sub UpdateIndex()
Dim RowNumber As Integer, LookUpLast As Integer
Dim Control As String, PartNumber As String, PartDescription As String, Rev As String
Dim LookUp As Range, IndexLookup As Variant
Control = "Arbitrary"
RowNumber = 2
With ThisWorkbook.Worksheets(Sheet2)
LookUpLast = .Range("A" & Rows.Count).End(xlUp).Row
Set LookUp = .Range(.Cells(2, 1), .Cells(LookUpLast, 2)) 'Had previously tried Range("A2", "B" & LookUpLast)
End With
IndexLookup = Application.WorksheetFunction.VLookUp(Control, LookUp, 2, False)
With ThisWorkbook.Worksheets(Sheet1)
.Range("D" & RowNumber) = IndexLookup
End With
RowNumber = RowNumber + 1
Next File
Exit Sub
ErrorHandler:
If Err.Number = 1004 Then
IndexLookup = "N/A"
Resume Next
End If
End Sub

Excel VBA - select, get and set data in Table

I've got a worksheet with a lot of tables in them and I'm just starting to use tables because they seem pretty handy. But I've never manipulated content in an Excel table before. And these tables are basically lists of columns with Firstname and Lastname. Based on the values on these columns, I want to generate a username. But I'm trying to write a generic Sub that takes arguments, such as worksheet and name of the table.
Previously I've done this when the data has not been in a table:
Cells(2, 2).Select
Do
strFirstName = ActiveCell.Value
strLastName = ActiveCell.Offset(0, 2).Value
strFirstName = Left(strFirstName, 1)
strUserName = strFirstName & strLastName
strUserName = LCase(strUserName)
ActiveCell.Offset(0, 5).Value = strUserName
ActiveCell.Offset(1, 0).Select
Loop Until IsEmpty(ActiveCell)
And now I'm trying to do the exact same thing, only with data from a Table. Any ideas? I've added a watch for "ActiveSheet" to see if I can find the tables, and they seem to be in ActiveSheet.ListObjects, but I couldn't see any .Select option there. Perhaps I don't need to select the Table in order to manipulate it's content?
When looping over a range (whether in a table or in a range) it is usually faster to copy the data to a variant array, manipulate that array, and then copy the result back to the sheet.
Sub zz()
Dim oUsers As ListObject
Dim v As Variant
Dim vUserName() As Variant
Dim i As Long
Dim colFirst As Long
Dim colLast As Long
Dim colUser As Long
Set oUsers = ActiveSheet.ListObjects(1)
colFirst = oUsers.ListColumns("FirstName").Index
colLast = oUsers.ListColumns("LastName").Index
colUser = oUsers.ListColumns("UserName").Index
v = oUsers.DataBodyRange
ReDim vUserName(1 To UBound(v, 1), 1 To 1)
For i = 1 To UBound(v, 1)
vUserName(i, 1) = LCase(Left(v(i, colFirst), 1) & v(i, colLast))
Next
oUsers.ListColumns("UserName").DataBodyRange = vUserName
End Sub
If you really want to loop over the range itself:
For i = 1 To oUsers.ListRows.Count
oUsers.ListColumns("UserName").DataBodyRange.Rows(i) = LCase(Left( _
oUsers.ListColumns("FirstName").DataBodyRange.Rows(i), 1) & _
oUsers.ListColumns("LastName").DataBodyRange.Rows(i))
Next
For this situation you could also just use a formula in the UserName column itself, with no vba required
=LOWER(LEFT([#FirstName],1)&[#LastName])
EDIT
Sorry, don't know of a Formula way to remove any of a list of characters from a string. You might have to revert to vba for this. Here's a user defined function to do it. Your formula will become
=DeleteChars([#UserName],{"$","#"})
To Delete the characters replace {"$","#"} with a array list of characters you want to remove (you can make the list as long as you need)
To replace the characters use {"$","#";"X","X"} where the list up to the ; is the old characters, after the ; the new. Just make sure the listsa are the same length.
UDF code:
Function DeleteChars(r1 As Range, ParamArray c() As Variant) As Variant
Dim i As Long
Dim s As String
s = r1
If UBound(c(0), 1) = 1 Then
For i = LBound(c(0), 2) To UBound(c(0), 2)
s = Replace(s, c(0)(1, i), "")
Next
Else
For i = LBound(c(0), 2) To UBound(c(0), 2)
s = Replace(s, c(0)(1, i), c(0)(2, i))
Next
End If
DeleteChars = s
End Function