I've seen a macro here that works well for filtering and copying data into a new tab. However, it doesn't work when I try to change the filtered column (in this case is column F, but I want to change to column B). See below:
Function GetWorksheet(shtName As String) As Worksheet
On Error Resume Next
Set GetWorksheet = Worksheets(shtName)
End Function
Sub filter()
Application.ScreenUpdating = False
Dim x As Range
Dim rng As Range
Dim last As Long
Dim sht As String
'specify sheet name in which the data is stored
sht = "Sheet1"
'change filter column in the following code
last = Sheets(sht).Cells(Rows.Count, "F").End(xlUp).Row
Set rng = Sheets(sht).Range("A1:F" & last)
Sheets(sht).Range("F1:F" & last).AdvancedFilter Action:=xlFilterCopy, CopyToRange:=Range("AA1"), Unique:=True
For Each x In Range([AA2], Cells(Rows.Count, "AA").End(xlUp))
With rng
.AutoFilter
.AutoFilter Field:=6, Criteria1:=x.Value
.SpecialCells(xlCellTypeVisible).Copy
Sheets.Add(After:=Sheets(Sheets.Count)).Name = x.Value
ActiveSheet.Paste
End With
Next x
' Turn off filter
Sheets(sht).AutoFilterMode = False
With Application
.CutCopyMode = False
.ScreenUpdating = True
End With
End Sub
I'd say if you want this to be re-usable for different sheets then one more layer of abstraction would be useful, so you can call the same sub but with different source ranges:
Sub Tester()
With ThisWorkbook.Worksheets("Sheet1")
FilterRangeToNewSheets .Range("A1:F" & .Cells(.Rows.Count, "F").End(xlUp).Row), 6
End With
End Sub
'Given a range and a column index in that range, add new sheets, one for each
' set of unique values in the range
Sub FilterRangeToNewSheets(rngToFilter As Range, filterColumnIndex As Long)
Dim vals As Collection, k, wb As Workbook
Set wb = rngToFilter.Worksheet.Parent 'parent workbook
Set vals = Uniques(rngToFilter.Columns(filterColumnIndex).Offset(1, 0)) 'offset to exclude the header
For Each k In vals
With rngToFilter
.AutoFilter
.AutoFilter Field:=filterColumnIndex, Criteria1:=k
.SpecialCells(xlCellTypeVisible).Copy
With wb.Worksheets.Add(After:=wb.Worksheets(wb.Worksheets.Count))
.Name = k
.Paste Destination:=.Range("A1")
End With
End With
Next k
rngToFilter.Worksheet.AutoFilterMode = False ' Turn off filter
Application.CutCopyMode = False
End Sub
'extract all unique values from a range into a dictionary
Function Uniques(rng As Range) As Collection
Dim col As New Collection, data, c As Range, v
For Each c In rng.Cells
v = c.Value
If Len(v) > 0 Then
On Error Resume Next 'ignore any duplicate key error
col.Add v, v
On Error GoTo 0 'stop ignoring errors
End If
Next c
Set Uniques = col
End Function
Swapped out your Advanced Filter for a function which will return a Collection containing only unique values.
Related
I am trying to sort a table by deleting rows that have their cell in column 9 NOT beginning with S, X, or P. Below is the code that I have that filters for the rows that do not meet my criteria, and then deletes them, and then shows the remaining values.
Range("I:I").NumberFormat = "#"
lo.Range.AutoFilter Field:=9, Criteria1:=Array("<>S*", "<>X*", "<>P*"), Operator:=xlOr
Application.DisplayAlerts = False
lo.DataBodyRange.SpecialCells(xlCellTypeVisible).Delete
Application.DisplayAlerts = True
lo.AutoFilter.ShowAllData
Currently, regardless or order, only rows that contain the last criteria in the array are kept.
Delete Multi-Criteria Rows of an Excel Table
You cannot have more than two criteria (elements) with wild characters.
As a workaround, this solution adds a new column and writes a formula to it. The formula returns a boolean indicating whether a string starts with the chars from the list. Then it filters the new column by False and deletes these filtered tables' (not worksheet's) rows. Finally, it deletes the new column.
The data to the right (one empty column is assumed) stays intact, it is not shifted in any way hence the inserting and deleting of a worksheet column instead of using .ListColumns.Add.
Adjust the values in the constants section.
Option Explicit
Sub DeleteMultiCriteriaRows()
Const wsName As String = "Sheet1"
Const tblName As String = "Table1"
Const NotFirstCharList As String = "s,x,p"
Const CritCol As Long = 9
' Extract chars for the formula.
Dim Nfc() As String: Nfc = Split(NotFirstCharList, ",")
Dim NotFirstChar As String: NotFirstChar = "{"
Dim n As Long
For n = 0 To UBound(Nfc)
NotFirstChar = NotFirstChar & """" & Nfc(n) & ""","
Next n
NotFirstChar = Left(NotFirstChar, Len(NotFirstChar) - 1) & "}"
Erase Nfc
Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
Dim ws As Worksheet: Set ws = wb.Worksheets(wsName)
Dim tbl As ListObject: Set tbl = ws.ListObjects(tblName)
Application.ScreenUpdating = False
With tbl
If Not .ShowAutoFilter Then .ShowAutoFilter = True
If .AutoFilter.FilterMode Then .AutoFilter.ShowAllData ' remove filter
.ListColumns(CritCol).DataBodyRange.NumberFormat = "#" ' ?
Dim nFormula As String
nFormula = "=ISNUMBER(MATCH(LEFT(" & .Name & "[#" _
& .ListColumns(CritCol).Name & "],1)," & NotFirstChar & ",0))"
Dim LastCol As Long: LastCol = .ListColumns.Count
With .ListColumns(1) ' write formulas to newly inserted column
.Range.Offset(, LastCol).EntireColumn.Insert
.DataBodyRange.Offset(, LastCol).Formula = nFormula
End With
LastCol = LastCol + 1 ' think new column
.Range.AutoFilter LastCol, False ' think Not(FirstChar)
Dim vrg As Range ' Visible Range
On Error Resume Next ' prevent 'No cells found...' error
Set vrg = .DataBodyRange.SpecialCells(xlCellTypeVisible)
On Error GoTo 0
.AutoFilter.ShowAllData ' remove filter
If Not vrg Is Nothing Then ' delete visible rows
vrg.Delete Shift:=xlShiftUp
End If
.ListColumns(LastCol).Range.EntireColumn.Delete ' delete new column
End With
Application.ScreenUpdating = True
End Sub
This code will delete any rows that have a value in the 9th column of the first table on the first sheet in a workbook that doesn't start with one of the letters in arrBeginsWith.
There are other ways to do achieve what you want, for example adding a helper column that identifies the rows to delete with a formula and then filtering on that column.
Option Explicit
Sub KeepRowsStartingWith()
Dim tbl As ListObject
Dim rngDelete As Range
Dim arrBeginsWith As Variant
Dim arrData As Variant
Dim idxRow As Long
Dim StartRow As Long
Dim Res As Variant
Set tbl = Sheets(1).ListObjects(1)
With tbl.ListColumns(9).DataBodyRange
StartRow = .Cells(1, 1).Row
arrData = .Value
End With
ReDim arrDeleteRows(1 To UBound(arrData, 1))
arrBeginsWith = Array("S", "X", "P")
For idxRow = 1 To UBound(arrData, 1)
Res = Application.Match(Left(arrData(idxRow, 1), 1), arrBeginsWith, 0)
If IsError(Res) Then
If rngDelete Is Nothing Then
Set rngDelete = Intersect(tbl.DataBodyRange, Sheets(1).Rows(idxRow + StartRow - 1))
Else
Set rngDelete = Union(rngDelete, Intersect(tbl.DataBodyRange, Sheets(1).Rows(idxRow + StartRow - 1)))
End If
End If
Next idxRow
rngDelete.Delete xlShiftUp
End Sub
I ended up creating a new column in my table with an if statement to identify if a cell began with a letter or number. Then I filtered for the rows that had a number, deleted those rows, and then showed the remaining rows. I then deleted the helper column as to not have to deal with it later.
ThisWorkbook.Worksheets("Aluminum Futures").Columns("T:T").Select
Selection.Insert Shift:=xlToRight
Range("T1") = "Letter/Number"
Range("T2").Select
ActiveCell.FormulaR1C1 = "=IF(ISERR(LEFT(RC[-11],1)*1),""letter"",""number"")"
Range("T2").Select
Selection.AutoFill Destination:=Range("PF[Letter/Number]")
Range("PF[Letter/Number]").Select
lo.Range.AutoFilter Field:=20, Criteria1:="number"
Application.DisplayAlerts = False
lo.DataBodyRange.SpecialCells(xlCellTypeVisible).Delete
Application.DisplayAlerts = True
lo.AutoFilter.ShowAllData
Columns("T:T").Delete
I use VBA rarely and am always re-learning. This is my first posting.
I am using OCR to pull in tables from PDFs to individual worksheets (usually 100-200 tabs) and I have VBA programming ready to consolidate the data based on header values. But the headers are error prone and need to be reviewed first. I want to run a VBA macro that tests headers in row 1 against a set list and highlight those headers that exactly match.
I found a great start with Conditional formatting over huge range in excel, using VBA (Dictionary Approach) which tests lists, but I am struggling to convert the code to handle rows instead of columns. (Next I plan to have it run on every tab in the workbook, but am stuck at the testing stage).
Here is my current edit of the original code to pull from rows, but I get a subscript out of range on If dict2.Exists(vals(i)) Then
Option Explicit
Sub main3()
Dim mainRng As Range, list1Rng As Range
Dim mainDict As New Scripting.Dictionary, list1Dict As New
Scripting.Dictionary 'Main is Header and list1 is prescribed header list
Set mainRng = GetRange(Worksheets("Main"), "1") '<--| get "Main" sheet row "1" range from column A right to last non empty column
Set list1Rng = GetRange(Worksheets("list1"), "1") '<--| get "list1" sheet row "1" range from column A right to last non empty column
Set mainDict = GetDictionary(mainRng)
Set list1Dict = GetDictionary(list1Rng)
ColorMatchingRange2 list1Rng, list1Dict, mainDict
End Sub
Sub ColorMatchingRange2(rng1 As Range, dict1 As Scripting.Dictionary, dict2 As Scripting.Dictionary)
Dim unionRng As Range
Dim vals As Variant
Dim i As Long
vals = rng1.Value 'oringinal code transposed with = Application.Transpose(rng1.Value)
Set unionRng = rng1.Offset(rng1.Rows.Count).Resize(1, 1)
For i = LBound(vals) To UBound(vals)
If dict2.Exists(vals(i)) Then Set unionRng = Union(unionRng, rng1(1, i))
Next i
Set unionRng = Intersect(unionRng, rng1)
If Not unionRng Is Nothing Then
With unionRng.Interior
.PatternColorIndex = xlAutomatic
.Color = 65535
.TintAndShade = 0
End With
End If
End Sub
Function GetDictionary(rng As Range) As Scripting.Dictionary
Dim dict As New Scripting.Dictionary
Dim vals As Variant
Dim i As Long
vals = rng.Value 'oringinal code transposed with=Application.Transpose(rng.Value)
On Error Resume Next
For i = LBound(vals) To UBound(vals)
dict.Add vals(i), rng(1, i).Address
Next i
On Error GoTo 0
Set GetDictionary = dict
End Function
Function GetRangeRow(ws As Worksheet, rowIndex As String) As Range
With ws '<--| reference passed worksheet
Set GetRangeRow = .Range("A" & rowIndex, .Cells(1, .Columns.Count).End(xlToLeft)) '<--| set its row "rowIndex" range from row 1 right to last non empty column
End With
End Function
More background, the VBA will be in a Control Workbook with the set header list, and the code will run on the ActiveWorkbook which will be the data across many worksheets, but I believe I've got that figured out.
Simpler approach:
Sub HighlightMatchedHeaders()
Dim rngList As Range, c As Range, v
Dim sht As Worksheet, wb As Workbook
Set wb = ActiveWorkbook 'or whatever
'set the lookup list
With wb.Sheets("list")
Set rngList = .Range("A1:A" & .Cells(Rows.Count, 1).End(xlUp).Row)
End With
For Each sht In wb.Worksheets
'ignore the "list" sheet
If sht.Name <> rngList.Worksheet.Name Then
'checking row 1
For Each c In Application.Intersect(sht.Rows(1), sht.UsedRange).Cells
v = Trim(c.Value)
If Len(v) > 0 Then
'has a header: check for match
If Not IsError(Application.Match(v, rngList, 0)) Then
c.Interior.Color = vbRed 'show match
End If
End If
Next c
End If
Next sht
End Sub
I am using the AdvancedFilter property of a Range object to copy a unique set of values to another range within my workbook. Unfortunately, the ActiveSheet has an autofilter applied and the AdvancedFilter statement removes the autofilter from the ActiveSheet.
As you will see in my code below I can add the autofilter back onto the ActiveSheet but this feels a little 'clunky'.
Could anyone suggest an alternative coding solution?
Sub mmDropDownClasses()
'Populate the 'LU' sheet with a unique range of classes from the currently
'active sheet
Range("LU!I2:I30").ClearContents 'Clear the range to be populated
ActiveSheet.Unprotect 'Unprotect the active sheet
'Extract the unique values from a range on the active sheet and copy them
'to a range on the 'LU' sheet
ActiveSheet.Range("C6:C304").AdvancedFilter Action:=xlFilterCopy,
CopyToRange:=Range("LU!I2"), Unique:=True
'Reinstate the autofilter deleted by the advancedfilter in the previous
'statement
ActiveSheet.Range("A5:BA5").AutoFilter
ActiveSheet.Protect AllowFiltering:=True 'Protect the active sheet
'Sort the range on the 'LU' sheet
Range("LU!I2:I30").Sort key1:=Range("LU!I2:I30"), order1:=xlAscending
End Sub
Here's an example of Dictionary use:
Sub testit()
Dim v
v = UniqueListFromRange(ActiveSheet.Range("C6:C304"))
Sheets("LU").Range("I2").Resize(UBound(v) + 1).Value = Application.Transpose(v)
End Sub
Public Function UniqueListFromRange(rgInput As Range) As Variant
Dim d As Object
Dim rgArea As Excel.Range
Dim dataSet
Dim x As Long
Dim y As Long
Set d = CreateObject("Scripting.Dictionary")
For Each rgArea In rgInput.Areas
dataSet = rgArea.Value
If IsArray(dataSet) Then
For x = 1 To UBound(dataSet)
For y = 1 To UBound(dataSet, 2)
If Len(dataSet(x, y)) <> 0 Then d(dataSet(x, y)) = Empty
Next y
Next x
Else
d(dataSet) = Empty
End If
Next rgArea
UniqueListFromRange = d.keys
End Function
I have this issue that I'm trying to solve. each day I get an report containing data that I need to send forward. So in order to make it a bit easier I have tried to find a macro that creates a new sheet with the name of the agent and moves the data for each agent in the created sheet...
I have found one that suppose to do pretty much that. But since this isn't really my area of expertise I'm not able to modify it to handle my request, and even make it work probably. Anyone have any idea ?
Const cl& = 2
Const datz& = 1
Dim a As Variant, x As Worksheet, sh As Worksheet
Dim rws&, cls&, p&, i&, ri&, j&
Dim u(), b As Boolean, y
Application.ScreenUpdating = False
Sheets("Sheet1").Activate
rws = Cells.Find("*", , , , xlByRows, xlPrevious).Row
cls = Cells.Find("*", , , , xlByColumns, xlPrevious).Column
Set x = Sheets.Add(After:=Sheets("Sheet1"))
Sheets("Sheet1").Cells(1).Resize(rws, cls).Copy x.Cells(1)
Set a = x.Cells(1).Resize(rws, cls)
a.Sort a(1, cl), 2, Header:=xlYes
a = a.Resize(rws + 1)
p = 2
For i = p To rws + 1
If a(i, cl) <> a(p, cl) Then
b = False
For Each sh In Worksheets
If sh.Name = a(p, cl) Then b = True: Exit For
Next
If Not b Then
Sheets.Add.Name = a(p, cl)
With Sheets(a(p, cl))
x.Cells(1).Resize(, cls).Copy .Cells(1)
ri = i - p
x.Cells(p, 1).Resize(ri, cls).Cut .Cells(2, 1)
.Cells(2, 1).Resize(ri, cls).Sort .Cells(2, datz), Header:=xlNo
y = .Cells(datz).Resize(ri + 1)
ReDim u(1 To 2 * ri, 1 To 1)
For j = 2 To ri
u(j, 1) = j
If y(j, 1) <> y(j + 1, 1) Then u(j + ri, 1) = j
Next j
.Cells(cls + 1).Resize(2 * ri) = u
.Cells(1).Resize(2 * ri, cls + 1).Sort .Cells(cls + 1), Header:=xlYes
.Cells(cls + 1).Resize(2 * ri).ClearContents
End With
End If
p = i
End If
Next i
Application.DisplayAlerts = False
x.Delete
Application.DisplayAlerts = True
Application.ScreenUpdating = True
This is an example of my report I receive
example
I keep getting error on row: a.Sort a(1, cl), 2, Header:=xlYes
That in self i don't really know what it does. Can anyone explain?
Here is a generic model (heavily commented) that should produce your individual agent worksheets. This copies the original 'master' worksheet and removes information that does not pertain to each individual agent.
Module1 code
Option Explicit
Sub agentWorksheets()
Dim d As Long, agnt As Variant, vAGNTs As Variant, dAGNTs As Object
Dim wsn As String, wb As Workbook
'set special application environment
'appTGGL bTGGL:=False 'uncomment this after debuging is complete
Set wb = ThisWorkbook '<~~ set to any open workbook or open a closed one
wsn = "Agents" '<~~ rename to the right master workbook
'create the dictionary and
Set dAGNTs = CreateObject("Scripting.Dictionary")
dAGNTs.CompareMode = vbTextCompare
'first the correct workbook
With wb
'work with the master worksheet
With .Worksheets(wsn)
'get all of the text values from column B
vAGNTs = .Range(.Cells(6, "B"), .Cells(Rows.Count, "B").End(xlUp)).Value2
'construct a dictionary of the agents usin unique keys
For d = LBound(vAGNTs) To UBound(vAGNTs)
'overwrite method - no check to see if it exists (just want unique list)
dAGNTs.Item(vAGNTs(d, 1)) = vbNullString
Next d
End With
'loop through the agents' individual worksheets
'if one does not exist, create it from the master workbook
For Each agnt In dAGNTs
'set error control to catch non-existant agent worksheets
On Error GoTo bm_Need_Agent_WS
With Worksheets(agnt)
On Error GoTo bm_Safe_Exit
'if an agent worksheet did not exist then
'one has been created with non-associated data removed
'perform any additional operations here
'example: today's date in A1
.Cells(1, "A") = Date
End With
Next agnt
End With
'slip past agent worksheet creation
GoTo bm_Safe_Exit
bm_Need_Agent_WS:
'basic error control for bad worksheet names, etc.
On Error GoTo 0
'copy the master worksheet
wb.Worksheets(wsn).Copy after:=Sheets(Sheets.Count)
With wb.Worksheets(Sheets.Count)
'rename the copy to the agent name
.Name = StrConv(agnt, vbProperCase)
'turn off any existing AutoFilter
If .AutoFilterMode Then .AutoFilterMode = False
'filter on column for everything that isn't the agent
With .Range(.Cells(5, "B"), .Cells(Rows.Count, "B").End(xlUp))
.AutoFilter field:=1, Criteria1:="<>" & agnt
'step off the header row
With .Resize(.Rows.Count - 1, 1).Offset(1, 0)
'check if there is anything to remove
If CBool(Application.Subtotal(103, .Cells)) Then
'delete all non-associated information
.EntireRow.Delete
End If
End With
End With
'turn off the AutoFilter we just created
.AutoFilterMode = False
End With
'go back to the thrown error
Resume
bm_Safe_Exit:
'reset application environment
appTGGL
End Sub
'helper sub to set/restore all of the environment settings
Public Sub appTGGL(Optional bTGGL As Boolean = True)
With Application
.ScreenUpdating = bTGGL
.EnableEvents = bTGGL
.DisplayAlerts = bTGGL
.Calculation = IIf(bTGGL, xlCalculationAutomatic, xlCalculationManual)
.CutCopyMode = False
.StatusBar = vbNullString
End With
Debug.Print Timer
End Sub
Sometimes it is just easier to remove what you do not want than recreate many parts of what you started with.
With #Jeeped great answer, I will also add second answer. :-)
To separate each agent data to separate sheets you can do the following...
see comment on the code
Option Explicit
Sub Move_Each_Agent_to_Sheet()
' // Declare your Variables
Dim Sht As Worksheet
Dim Rng As Range
Dim List As Collection
Dim varValue As Variant
Dim i As Long
' // Set your Sheet name
Set Sht = ActiveWorkbook.Sheets("Sheet1")
' // set your auto-filter, A6
With Sht.Range("A6")
.AutoFilter
End With
' // Set your agent Column range # (2) that you want to filter it
Set Rng = Range(Sht.AutoFilter.Range.Columns(2).Address)
' // Create a new Collection Object
Set List = New Collection
' // Fill Collection with Unique Values
On Error Resume Next
For i = 2 To Rng.Rows.Count
List.Add Rng.Cells(i, 1), CStr(Rng.Cells(i, 1))
Next i
' // Start looping in through the collection Values
For Each varValue In List
' // Filter the Autofilter to macth the current Value
Rng.AutoFilter Field:=2, Criteria1:=varValue
' // Copy the AutoFiltered Range to new Workbook
Sht.AutoFilter.Range.Copy
Worksheets.Add.Paste
ActiveSheet.Name = Left(varValue, 30)
Cells.EntireColumn.AutoFit
' // Loop back to get the next collection Value
Next varValue
' // Go back to main Sheet and removed filters
Sht.AutoFilter.ShowAllData
Sht.Activate
End Sub
I am trying to delete duplicate values in a temporary list based on a value in a combobox. The code below loops through individual rows to check whether a value matches. It is slow.
Dim ws As Worksheet
Dim i As Long
Set ws = Sheets("TempList3")
On Error Resume Next
For i = Cells(Rows.Count, 2).End(xlUp).Row To 1 Step -1
If Cells(i, 2) <> Sheets("Sheet1").ComboBox2.Value Then
ws.Rows(i).EntireRow.Delete
End If
Next
Is there a way to evaluate the entire column's values against the combobox's value once and then delete all rows on a worksheet. Or perhaps there is a better way?
I used a looping Find function, it deletes the row where the value was found and then it searches again and deletes the next row it finds until it can no longer find the Combo value on the sheet:
Sub find_cell()
Dim find_cell As Range
Set ws = Sheets("TempList3")
stop_loop = False
Do Until stop_loop = True
Set find_cell = ws.Cells.Find(What:=Sheets("Sheet1").ComboBox2.Value, LookAt:=xlWhole)
If Not find_cell Is Nothing Then
ws.Rows(find_cell.Row).EntireRow.Delete
Else
stop_loop = True
End If
Loop
End Sub
Not knowing how many rows you are talking about, I used 10 thousand for my example codes. here are two examples, try the both and see what works best for you.
You can run through the column and unionize the range found, then delete the rows, for example.
See here for example workbook
Sub UnIonRng()
Dim FrstRng As Range
Dim UnIonRng As Range
Dim c As Range, s As String
s = Sheets("Sheet1").ComboBox2
Set FrstRng = Range("B:B").SpecialCells(xlCellTypeConstants, 23)
For Each c In FrstRng.Cells
If c = s Then
If Not UnIonRng Is Nothing Then
Set UnIonRng = Union(UnIonRng, c) 'adds to the range
'MsgBox UnionRng.Address 'remove later
Else
Set UnIonRng = c
End If
End If
Next c
UnIonRng.EntireRow.Delete
End Sub
Or you can try to filter the column B and delete the rows that way:
Sub FilterDeleteRow()
Dim ws As Worksheet
Dim LstRw As Long, Rng As Range, s As String, x
Set ws = Sheets("TempList3")
s = Sheets("Sheet1").ComboBox2
Application.ScreenUpdating = 0
With ws
LstRw = .Cells(.Rows.Count, "B").End(xlUp).Row
x = Application.WorksheetFunction.CountIf(.Range("B:B"), s)
If x > 0 Then
Columns("B:B").AutoFilter Field:=1, Criteria1:=s
Set Rng = .Range("B2:B" & LstRw).SpecialCells(xlCellTypeVisible)
Rng.EntireRow.Delete
.AutoFilterMode = 0
Else: MsgBox "Not Found"
End If
End With
End Sub