Split Workbook into multiple workbooks based on two columns - vba

I hope everyone is well.
I am look for some help. I am looking to automate a workbook which splits the data from the master file to the individual workbooks based on column H. What needs to be done first is that Column T needs to be filtered to 'Owned' or 'Impacted'. Column H then needs to be split into the separate workbooks. based on what may be in column H. On each new workbook created, whatever is under column H there needs to be two tabs, one tab for 'Owned' and one tab for 'Impacted'. This would need to be then saved as whatever the name of the cell was and the date.
The additional difficult bit is under column H, in each cell as per the attached there could be A, B, C, D, E, F as individual cells, but there could also be cells with multiple letters in them. If they have multiple letters each one needs to go into all the workbooks that are mentioned in the cell. So, for example if there is a cell with A, B, C, D, this would mean it would have to go into the workbook for the individual workbooks for A, B, C, and D.
I have attached the file image and I have the below code which I used. It does work, however due to the above issue with the multiple criteria in the cells it is splitting the workbooks further into individual workbooks. Does anyone know if a drop down can be added where I can select the criteria from column H and T, or another work around please. I am happy to try another code if necessary. Example workbook attached as well.
Option Explicit
Sub ParseItems()
'Based on selected column, data is filtered to individual workbooks
'workbooks are named for the value plus today's date
Dim LR As Long, Itm As Long, MyCount As Long, vCol As Long
Dim ws As Worksheet, MyArr As Variant, vTitles As String, SvPath As String
'Sheet with data in it
Set ws = Sheets("Master")
'Path to save files into, remember the final \
SvPath = "\\My Documents\New folder\"
'Range where titles are across top of data, as string, data MUST
'have titles in this row, edit to suit your titles locale
vTitles = "A1:V1"
'Choose column to evaluate from, column A = 1, B = 2, etc.
vCol = Application.InputBox("What column to split data by? " & vbLf _
& vbLf & "(A=1, B=2, C=3, etc)", "Which column?", 8, Type:=1)
If vCol = 0 Then Exit Sub
'Spot bottom row of data
LR = ws.Cells(ws.Rows.Count, vCol).End(xlUp).Row
'Speed up macro execution
Application.ScreenUpdating = False
'Get a temporary list of unique values from key column
ws.Columns(vCol).AdvancedFilter Action:=xlFilterCopy,
CopyToRange:=ws.Range("HH1"), Unique:=True
'Sort the temporary list
ws.Columns("HH:HH").Sort Key1:=ws.Range("HH2"), Order1:=xlAscending,
Header:=xlYes, _
OrderCustom:=1, MatchCase:=False, Orientation:=xlTopToBottom,
DataOption1:=xlSortNormal
'Put list into an array for looping (values cannot be the result of
formulas, must be constants)
MyArr = Application.WorksheetFunction.Transpose(ws.Range("HH2:HH" &
Rows.Count).SpecialCells(xlCellTypeConstants))
'clear temporary worksheet list
ws.Range("HH:HH").Clear
'Turn on the autofilter, one column only is all that is needed
ws.Range(vTitles).AutoFilter
'Loop through list one value at a time
For Itm = 1 To UBound(MyArr)
ws.Range(vTitles).AutoFilter Field:=vCol, Criteria1:=MyArr(Itm)
ws.Range("A1:A" & LR).EntireRow.Copy
Workbooks.Add
Range("A1").PasteSpecial xlPasteAll
Cells.Columns.AutoFit
MyCount = MyCount + Range("A" & Rows.Count).End(xlUp).Row - 1
ActiveWorkbook.SaveAs SvPath & MyArr(Itm) & Format(Date, " MM-DD-YY") &
".xlsx", 51 'use for Excel 2007+
ActiveWorkbook.Close False
ws.Range(vTitles).AutoFilter Field:=vCol
Next Itm
'Cleanup
ws.AutoFilterMode = False
MsgBox "Rows with data: " & (LR - 1) & vbLf & "Rows copied to other sheets:
" & MyCount & vbLf & "Hope they match!!"
Application.ScreenUpdating = True
End Sub
Any help would be appreciated. Thank in advance

Rather than applying filters to the worksheet you could load the entire dataset into an array and then store the row index #s for each of the various criteria. You can then use the row index lists to slice the array for each respective output.
I don't have your source data (couldn't see the attached file) but would this approach work?
Sub VariableCollections()
Dim HeaderVals() As Variant
Dim SourceData() As Variant, Criteria As Variant
Dim RowIndexLists As New Collection, ColIndexList As String
Dim KeyStore As New Collection, Key As Variant
Dim i As Long, Temp As String
Dim fName As String, fFormat As Long
Dim OutputArr() As Variant
On Error GoTo ErrorHandler
Application.ScreenUpdating = False
With Sheets("Master") 'change if necessary
'store table header values in array (A1:W1)
HeaderVals = .Cells(1, 1).Resize(, 23).Value
'store data in array, assume starts at A2
SourceData = .Cells(2, 1).Resize(.Cells(.Rows.Count, 1).End(xlUp).Row, 23).Value
End With
'index row #s for each Criteria & Owned/Impacted
For i = LBound(SourceData, 1) To UBound(SourceData, 1)
If SourceData(i, 23) = "Owned" Then 'col W
'loop each Criteria (col H) for current row
For Each Criteria In Split(SourceData(i, 8), ", ")
'test if key already added to KeyStore
If Not InCollection(KeyStore, Criteria) Then KeyStore.Add Criteria, Criteria
'test if Criteria already added to RowIndexLists
If InCollection(RowIndexLists, Criteria & "_Own") Then 'already added...
'...update row index value for current key
Temp = RowIndexLists(Criteria & "_Own")
RowIndexLists.Remove (Criteria & "_Own")
RowIndexLists.Add Temp & "," & i, Criteria & "_Own"
Else 'not already stored...
'...Create New Item
RowIndexLists.Add i, Criteria & "_Own"
End If
Next Criteria
ElseIf SourceData(i, 23) = "Impacted" Then 'col W
'loop each Criteria (col H) for current row
For Each Criteria In Split(SourceData(i, 8), ", ")
'test if key already added to KeyStore
If Not InCollection(KeyStore, Criteria) Then KeyStore.Add Criteria, Criteria
'test if Criteria already added to RowIndexLists
If InCollection(RowIndexLists, Criteria & "_Imp") Then 'already added...
'...update row index value for current key
Temp = RowIndexLists(Criteria & "_Imp")
RowIndexLists.Remove (Criteria & "_Imp")
RowIndexLists.Add Temp & "," & i, Criteria & "_Imp"
Else 'not already stored...
'...Create New Item
RowIndexLists.Add i, Criteria & "_Imp"
End If
Next Criteria
End If
Next i
'save in same directory as current workbook
fName = Split(ThisWorkbook.FullName, ".")(0)
'set file format # based on OS type
#If Mac Then
fFormat = 52
#Else
fFormat = 51
#End If
'assumes cols 8 (H) and 23 (W) are no longer needed in output
ColIndexList = "1,2,3,4,5,6,7,9,10,11,12,13,14,15,16,17,18,19,20,21,22"
'slice HeaderVals array for matching cols
HeaderVals = Application.Index(HeaderVals, 0, Split(ColIndexList, ","))
'write out to new workbooks
For Each Key In KeyStore
'create new workbook
With Workbooks.Add
'output "Owned" matches for current Criteria (key value) if exist
If InCollection(RowIndexLists, Key & "_Own") Then
'slice array to indexed rows
OutputArr = Application.Index(SourceData, _
Application.Transpose(Split(RowIndexLists(Key & "_Own"), ",")), _
Split(ColIndexList, ","))
'add new worksheet, rename & output data
With .Worksheets.Add(After:=.Sheets(.Sheets.Count))
'rename sheet
.Name = "Owned"
'test if OutputArr has 2 dimensions
If IsArray2D(OutputArr) Then '2D i.e. rows & cols
.Cells(1, 1).Resize(, UBound(OutputArr, 2)) = HeaderVals
.Cells(2, 1).Resize(UBound(OutputArr, 1), UBound(OutputArr, 2)) = OutputArr
Else '1D i.e. single row
.Cells(1, 1).Resize(, UBound(OutputArr)) = HeaderVals
.Cells(2, 1).Resize(, UBound(OutputArr)) = OutputArr
End If
End With
End If
'output "Impacted" matches for current Criteria (key value) if exist
If InCollection(RowIndexLists, Key & "_Imp") Then
'slice array to indexed rows
OutputArr = Application.Index(SourceData, _
Application.Transpose(Split(RowIndexLists(Key & "_Imp"), ",")), _
Split(ColIndexList, ","))
'add new worksheet, rename & output data
With .Worksheets.Add(After:=.Sheets(.Sheets.Count))
'rename sheet
.Name = "Impacted"
'test if OutputArr has 2 dimensions
If IsArray2D(OutputArr) Then '2D i.e. rows & cols
.Cells(1, 1).Resize(, UBound(OutputArr, 2)) = HeaderVals
.Cells(2, 1).Resize(UBound(OutputArr, 1), UBound(OutputArr, 2)) = OutputArr
Else '1D i.e. single row
.Cells(1, 1).Resize(, UBound(OutputArr)) = HeaderVals
.Cells(2, 1).Resize(, UBound(OutputArr)) = OutputArr
End If
End With
End If
'delete sheet1
Application.DisplayAlerts = False
.Sheets(1).Delete
Application.DisplayAlerts = True
'save file & close
.SaveAs fName & "_" & Key, fFormat
.Close
End With
Next Key
ErrorHandler: If Err.Number <> 0 Then MsgBox "Error # " & Err.Number & " " & Err.Description
Application.ScreenUpdating = True
End Sub
as #dwirony suggested it utilizes the Split function on col H to break apart the various criteria on each row and then stores the row # in a collection.
I realize a Dictionary would be a better suited here rather than using Collections, however as Dictionaries are Windows only I prefer to avoid them unless I know for certain the file will only ever be used on Windows. If this is the case then the above code could be simplified by switching the collections out for a dictionary.
#jeeped Excel creates base-1 arrays when directly assigning a Range object to an array. I've always assumed to make them similar to the (ROW,COL) addressing.
==== Edit 6/30 ====
Updated code to reflect changes to data layout:
Additional cols in data range
Owned/Impacted col moved to Col W
Adjusted Worksheet reference to match OPs request

Related

VBA Testing two values, if one is different, copy

I am having a fair amount of trouble with the code below:
Sub TestEmail()
Dim i As Long
Dim LastRow As Long
Dim a As Worksheet
Dim b As Worksheet
Dim strText
Dim ObjData As New MSForms.DataObject
Set a = Workbooks("Book2").Worksheets(1)
Set b = Workbooks("Book1").Worksheets(1)
LastRow = a.Cells(Rows.Count, "A").End(xlUp).Row
For i = 2 To LastRow
If Not IsError(Application.Match(a.Cells(i, 7).Value, b.Columns(3), 0)) And IsError(Application.Match(a.Cells(i, 4).Value, b.Columns(11), 0)) Then
a.Range("D" & i).Copy
ObjData.GetFromClipboard
strText = Replace(ObjData.GetText(), Chr(10), "")
b.Range("K" & ).Value = b.Range("K" & ).Value & " / " & strText
End If
Next i
End Sub
I face two problems, one has me stumped and the other is due to lack of knowledge:
The line after IF is supposed to check if two values (numbers) in both workbooks match, and if two other values (text) don't match. If all true, then it must copy a value from Book2 and add it to a cell in book1.
The problems are:
-The macro doesn't seem to recognise when the values match or not.
-In the last line before "End If", I don't know how to tell excel to copy the text into the cell that didn't match in the second check.
I am sorry if I am not clear enough, this is hard to explain.
I'm hoping one of the experts knows how to make this work.
Thanks in advance
You are using If Not condition 1 And condition 2, so you are saying that if it doesn't match both conditions, Then you run the code. What you want to make are Nested If Statements However, one is If and the other If Not
To copy you are missing the i After "K"&: b.Range("K" & i) = b.Range("K" & i).Value & " / " & strText
The Address of the Cells are inside the Range Function, which in your case would be:
//It is the cell of the email from the first Workbook tou are copying, where you input the column D
a.Range("D" & i).Copy
//Add to Workbook b in column K the value from Cell K#/value copied
b.Range("K" & i) = b.Range("K" & i).Value & " / " & strText
You can also make it like this: b.Range("K" & i) = b.Range("K" & i).Value & " / " & a.Range("D" & i)
This way you are matching lines, so only if the IDs are on the same rows on both Workbooks it will work. If they aren't, you will have to use Nesting Loops or .Find Function
EDIT:
If I understood it, the code below might work if you make some changes for your application, because i didn't have the data to test and columns, etc. Try to implement it.
LastRowa = a.Cells(Rows.Count, "A").End(xlUp).Row
LastRowb = b.Cells(Rows.Count, "A").End(xlUp).Row
For i = 2 To LastRowa
'Address of String to look for
LookForString = a.Worksheets(1).Cells(i, 4) '4 is the COLUMN_INDEX
'Range to look on Workbook a
With a.Worksheets(1).Range("D1:D" & LastRowa) 'choose column to look
'Function .Find String on book a
Set mail_a = .Find(LookForString, LookIn:=xlValues)
If Not mail_a Is Nothing Then
FirstAddress = mail_a.Address
Do ' Actions here
'Range to look on Workbook b
With b.Worksheets(1).Range("K1:K" & LastRowb) 'choose column to look
'Function .Find on Workbook b
Set mail_b = .Find(LookForString, LookIn:=xlValues)
If Not mail_b Is Nothing Then
FirstAddress = mail_b.Address
Do 'Actions
'Verify if two other values (text) don't match
If Not WRITE_MATCH_CONDITION_HERE Then
'No need to verify of they are equal because the .Find function used the same reference
'I will use .Cells with .Row and .Column just to show another way to do it and make it dynamic
b.Cells(mail_b.Adress.Row, mail_b.Adress.Column) = b.Cells(mail_b.Adress.Row, mail_b.Adress.Column).Value & " / " & a.Cells(mail_a.Adress.Row, mail_a.Adress.Column) 'choose columns
End If
Set mail_b = .FindNext(mail_b)
Loop While Not mail_b Is Nothing And mail_b.Address <> FirstAddress
End If
End With
Set mail_a = .FindNext(mail_a)
Loop While Not mail_a Is Nothing And mail_a.Address <> FirstAddress
End If
End With
Next i
End Sub
p.s.: The <> is missing on mail_a.Address <> FirstAddress and mail_b.Address <> FirstAddress, when i posted with

Enquoting a cell value in double quotes: Excel VBA Macro

I want to put double quotes inside all cells in a particular column.
I have wrote the code to put double quotes but the problem is it is putting 3 double quotes around the value.
For Each myCell In ActiveWorkbook.Sheets("Sheet1").Range("B:B")
If myCell.Value <> "" Then
myCell.Value = Chr(34) & myCell.Value & Chr(34)
End If
Next myCell
The basic requirement is to split the excel file according to column B and save them as CSV files.
In the split filed, the values of column B and D must be enclosed within double quotes.
Full Code :
Option Explicit
Sub ParseItems()
Dim LR As Long, Itm As Long, MyCount As Long, vCol As Long
Dim ws As Worksheet, MyArr As Variant, vTitles As String, SvPath As String
Dim myCell As Range, transCell As Range
'Sheet with data in it
Set ws = Sheets("Sheet1")
'Path to save files into, remember the final \
SvPath = "D:\SplitExcel\"
'Range where titles are across top of data, as string, data MUST
'have titles in this row, edit to suit your titles locale
'Inserting new row to act as title, copying the data from first row in title, row deleted after use
ws.Range("A1").EntireRow.Insert
ws.Rows(2).EntireRow.Copy
ws.Range("A1").Select
ws.Paste
vTitles = "A1:Z1"
'Choose column to evaluate from, column A = 1, B = 2, etc.
vCol = 2
If vCol = 0 Then Exit Sub
'Spot bottom row of data
LR = ws.Cells(ws.Rows.Count, vCol).End(xlUp).Row
'Speed up macro execution
Application.ScreenUpdating = False
'Get a temporary list of unique values from key column
ws.Columns(vCol).AdvancedFilter Action:=xlFilterCopy, CopyToRange:=ws.Range("EE1"), Unique:=True
'Sort the temporary list
ws.Columns("EE:EE").Sort Key1:=ws.Range("EE2"), Order1:=xlAscending, Header:=xlYes, _
OrderCustom:=1, MatchCase:=False, Orientation:=xlTopToBottom, DataOption1:=xlSortNormal
'Put list into an array for looping (values cannot be the result of formulas, must be constants)
MyArr = Application.WorksheetFunction.Transpose(ws.Range("EE2:EE" & Rows.Count).SpecialCells(xlCellTypeConstants))
'clear temporary worksheet list
ws.Range("EE:EE").Clear
'Turn on the autofilter, one column only is all that is needed
'ws.Range(vTitles).AutoFilter
'Loop through list one value at a time
For Itm = 1 To UBound(MyArr)
ws.Range(vTitles).AutoFilter Field:=vCol, Criteria1:=MyArr(Itm)
'transCell = ws.Range("A2:A" & LR)
ws.Range("A2:A" & LR).EntireRow.Copy
Workbooks.Add
Range("A1").PasteSpecial xlPasteAll
Cells.Columns.AutoFit
MyCount = MyCount + Range("A" & Rows.Count).End(xlUp).Row - 1
For Each myCell In ActiveWorkbook.Sheets("Sheet1").Range("B:B")
If myCell.Value <> "" Then
myCell.Value = Chr(34) & myCell.Value & Chr(34)
End If
Next myCell
ActiveWorkbook.SaveAs SvPath & "po" & MyArr(Itm) & ActiveWorkbook.Sheets("Sheet1").Range("D1") & "." & Date2Julian(Date), xlCSV, local:=False
ActiveWorkbook.Close False
ws.Range(vTitles).AutoFilter Field:=vCol
Next Itm
'Cleanup
ws.Rows(1).EntireRow.Delete
ws.AutoFilterMode = False
Application.ScreenUpdating = True
End Sub
Function Date2Julian(ByVal vDate As Date) As String
Date2Julian = Format(DateDiff("d", CDate("01/01/" _
+ Format(Year(vDate), "0000")), vDate) _
+ 1, "000")
End Function
Sample Input Data :
24833837 8013 70 1105
25057089 8013 75 1105
25438741 8013 60 1105
24833837 8014 70 1106
25057089 8014 75 1106
25438741 8014 60 1106
Expected Output is Two files created with following data
File 1 :
24833837,"8013",70,1105
25057089,"8013",75,1105
25438741,"8013",60,1105
File 2:
24833837,"8014",70,1106
25057089,"8014",75,1106
25438741,"8014",60,1106
Resultant Output :
File 1 :
24833837,"""8013""",70,1105
25057089,"""8013""",75,1105
25438741,"""8013""",60,1105
Same for File 2
Kindly help. :)
Afaik, there is no simple way to trick Excel into using quotes around numbers when using the normal "save as csv"-procedure. You can, however, use VBA to save in whatever csv format you like.
Take code example from https://support.microsoft.com/en-us/help/291296/procedure-to-export-a-text-file-with-both-comma-and-quote-delimiters-in-excel
Just add an if-statement to determine whether to use quotes or not
' Write current cell's text to file with quotation marks.
If WorksheetFunction.IsText(Selection.Cells(RowCount, ColumnCount)) Then
Print #FileNum, """" & Selection.Cells(RowCount, _
ColumnCount).Text & """";
Else
Print #FileNum, Selection.Cells(RowCount, _
ColumnCount).Text;
End If
The WorksheetFunction.IsText will recognize your numbers as text if they are entered with a preceding ' (single high quote)
You would need to adjust the example to export the range you want with the pre-given filename from your code.
This little sub will do as you need. Just give it a filename fname, range to export as csv rg and a column number column_with_quotes - so something like this but with a range to suit:
save_as_csv_with_optional_quotes SvPath & "po" & MyArr(Itm) & ActiveWorkbook.Sheets("Sheet1").Range("D1") & "." & Date2Julian(Date), Range("A1:C5"), 2
Here is the sub:
Sub save_as_csv_with_optional_quotes(fname As String, rg As Range, column_with_quotes As Long)
Dim ff, r, c As Long
Dim loutput, cl As String
ff = FreeFile
Open fname For Output As ff
For r = 1 To rg.Rows.Count
loutput = ""
For c = 1 To rg.Columns.Count
If loutput <> "" Then loutput = loutput & ","
cl = rg.Cells(r, c).Value
If c = column_with_quotes Then cl = Chr$(34) & cl & Chr$(34)
loutput = loutput & cl
Next c
Print #ff, loutput
Next r
Close ff
End Sub
the problem is this line.
myCell.Value = Chr(34) & myCell.Value & Chr(34)
The quotes you are adding are then being quoted again when you export as CSV, hence three quotes each side of the value. A better option I think would be to change the number format of the myCell to be Text, rather than number. I haven't tried this but I think changing it to this should help.
myCell.Value = Chr(39) & myCell.Value
Chr(39) is an apostrophe and when you enter it as the first character of a cell value it forces the format to be Text.

VBA-Excel Look for column names, return their number and use column letters in function

I'm quite new at VBA. I've used it in excel for a couple macros, but this one is way above my head.
I'm looking to create a macro that will find the appropriate column, then based on the value in this columns, changes the values in three other columns. I already have a static macro:
Sub AdjustForNoIntent()
'Adjusts columns Role(U) (to C-MEM), REV Profile Follow-up Date(AJ) (to N/A) and deletes Follow-up Date(Y) when column Survey: Intent to Participate = No
Dim lastrow As Long
Dim i As Long
lastrow = Range("AE" & Rows.Count).End(xlUp).Row
For i = 2 To lastrow
If Not IsError(Range("AE" & i).Value) Then
If Range("AE" & i).Value = "No" And Range("U" & i).Value = "MEM" Then
Range("U" & i).Value = "C-MEM"
Range("Y" & i).ClearContents
Range("AJ" & i).Value = "N/A"
ElseIf Range("AE" & i).Value = "No" And Range("U" & i).Value = "VCH" Then
Range("U" & i).Value = "C-VCH"
Range("Y" & i).ClearContents
Range("AJ" & i).Value = "N/A"
End If
End If
Next i
End Sub
But this is a shared workbook, so people are adding columns randomly and every time I need to go back to the code and modify the columns refereces. What I want is, for instance, to look for column with "Role" header in row A3 and to insert it where the macro looks for column "U". That way other users can add/delete columns but I won't have to modify the macro every time.
In other macros, I manage to have this thing working:
Function fnColumnNumberToLetter(ByVal ColumnNumber As Integer)
fnColumnNumberToLetter = Replace(Replace(Cells(1,ColumnNumber).Address, "1", ""), "$", "")
End Function
Dim rngColumn As Range
Dim ColNumber As Integer
Dim ColName As String
ColName = "Email Address"
Sheets("Tracking").Select
Set rngColumn = Range("3:3").Find(ColName)
ColNumber = Sheets("Tracking").Range(rngColumn, rngColumn).Column
Sheets("Combined").Range(ActiveCell, "W2").FormulaLocal = "=IF(ISERROR(INDEX(Tracking!$A:$A,MATCH(O:O,Tracking!" & fnColumnNumberToLetter(ColNumber) & ":" & fnColumnNumberToLetter(ColNumber) & ",0))), INDEX(Tracking!$A:$A,MATCH(U:U,Tracking!" & fnColumnNumberToLetter(ColNumber) & ":" & fnColumnNumberToLetter(ColNumber) & ",0)), INDEX(Tracking!$A:$A,MATCH(O:O,Tracking!" & fnColumnNumberToLetter(ColNumber) & ":" & fnColumnNumberToLetter(ColNumber) & ",0)))"
However, I am unable to link the latter to the first and much less to get it to find multiple columns. Any help is appreciated.
EDIT:
Following suggestions, here is the new code. Doesn't return an error, but doesn't do anything either. It loops through the c loop ok, but jumps from For i =2 ... line to End Sub.
Sub Adjust()
Dim lastrow As Long
Dim i As Long
Dim headers As Dictionary
Dim c As Long
Set headers = New Scripting.Dictionary
For c = 1 To Cells(3, Columns.Count).End(xlToLeft).Column
headers.Add Cells(3, c).Value, c
Next c
lastrow = Cells(headers.Item("Survey: Interest to Participate") & Rows.Count).End(xlUp).Row
For i = 2 To lastrow
If Not IsError(Cells(i, headers.Item("Survey: Interest to Participate")).Value) Then
If Cells(i, headers.Item("Survey: Interest to Participate")).Value = "No" And Cells(i, headers.Item("Role")).Value = "MEM" Then
Cells(i, headers.Item("Role")).Value = "C-MEM"
Cells(i, headers.Ittem(" Follow-up date")).ClearContents
Cells(i, headers.Item("REV profile follow-up date")).Value = "N/A"
ElseIf Cells(i, headers.Item("Survey: Interest to Participate")).Value = "No" And Cells(i, headers.Item("Role")).Value = "VCH" Then
Cells(i, headers.Item("Role")).Value = "C-VCH"
Cells(i, headers.Ittem(" Follow-up date")).ClearContents
Cells(i, headers.Item("REV profile follow-up date")).Value = "N/A"
End If
End If
Next i
End Sub
The way I'd go about this would be to create a Dictionary with header names as keys and column numbers as values:
Dim headers As Dictionary
Set headers = New Scripting.Dictionary
Dim c As Long
'Assuming headers are in row 1 for sake of example...
For c = 1 To Cells(1, Columns.Count).End(xlToLeft).Column
headers.Add Cells(1, c).Value, c
Next
Then, instead of using hard-code column letters with the Range, use the Cells collection and index it by column number using the Dictionary to look it up based on the header. For example, if your code expects column "U" to be under that header "Role" here:
Range("U" & i).Value = "C-MEM"
You can replace it with a column lookup like this using the Dictionary like this:
Cells(i, headers.Item("Role")).Value = "C-MEM"
Note that this requires a reference to the Microsoft Scripting Runtime (Tools->References... then check the box).
But this is a shared workbook, so people are adding columns randomly and every time I need to go back to the code and modify the columns refereces.
Protect the workbook to prevent this undesired behavior?
I would personally prefer to use Named Ranges, which will adjust with insertions and re-sorting of the data columns.
From Formulas ribbon, define a new name:
Then, confirm that you can move, insert, etc., with a simple procedure like:
Const ROLE As String = "Role"
Sub foo()
Dim rng As Range
Set rng = Range(ROLE)
' This will display $B$1
MsgBox rng.Address, vbInformation, ROLE & " located:"
rng.Offset(0, -1).Insert Shift:=xlToRight
' This will display $C$1
MsgBox rng.Address, vbInformation, ROLE & " located:"
rng.Cut
Application.GoTo Range("A100")
ActiveSheet.Paste
' This will display $A$100
MsgBox rng.Address, vbInformation, ROLE & " located:"
End Sub
So, I would define a Named Range for each of your columns (presently assumed to be AE, U, Y & AJ). The Named Range can span the entire column, which will minimize changes to the rest of your code.
Given 4 named ranges like:
Role, representing column U:U
RevProfile, representing column AJ:AJ
FollowUp, representing column Y:Y
Intent, representing column AE:AE
(NOTE: If you anticipate that users may insert rows above your header rows, then I would change the Named range assignments to only the header cells, e.g., "$AE$1", "$U$1", etc. -- this should require no additional changes to the code below)
You could do like this:
'Constant strings representing named ranges in this worksheet
Public Const ROLE As String = "Role"
Public Const REVPROFILE As String = "RevProfile"
Public Const FOLLOWUP As String = "FollowUp"
Public Const INTENT As String = "Intent"
Sub AdjustForNoIntent()
'Adjusts columns Role(U) (to C-MEM), REV Profile Follow-up Date(AJ) (to N/A) and deletes Follow-up Date(Y) when column Survey: Intent to Participate = No
Dim lastrow As Long
Dim i As Long
lastrow = Range(INTENT).End(xlUp).Row
For i = 2 To lastrow
If Not IsError(Range(INTENT).Cells(i).Value) Then
If Range(INTENT).Cells(i).Value = "No" And Range(ROLE).Cells(i).Value = "MEM" Then
Range(ROLE).Cells(i).Value = "C-MEM"
Range(FOLLOWUP).ClearContents
Range(REVPROFILE).Cells(i).Value = "N/A"
ElseIf Range(INTENT).Cells(i).Value = "No" And Range(ROLE).Cells(i).Value = "VCH" Then
Range(ROLE).Cells(i).Value = "C-VCH"
Range(FOLLOWUP).Cells(i).ClearContents
Range(REVPROFILE).Value = "N/A"
End If
End If
Next
End Sub
I would go with David Zemens answer but you could also use Range().Find to get the correct columns.
Here I refactored you code to find and set references to your column headers. Everything is based relative to these references.
Here I set a reference to Row 3 of the Survey column where your column header is:
Set rSurvey = .Rows(3).Find(What:="Survey", MatchCase:=False, Lookat:=xlWhole)
Because everything is relative to rSurvey the last row is = the actual last row - rSurvey's row
lastrow = rSurvey(.Rows.Count - rSurvey.Row).End(xlUp).Row - rSurvey.Row
Since rSurvey is a range we know that rSurvey.Cells(1, 1) is our column header. What isn't apparent is that since rSurvey is a range rSurvey(1, 1) is also our column header and since column and row indices are optional rSurvey(1) is also the column header cell.
Know all of that we can iterate over the cells in each column like this
For i = 2 To lastrow
rSurvey( i )
Sub AdjustForNoIntent()
'Adjusts columns Role(U) (to C-MEM), REV Profile Follow-up Date(AJ) (to N/A) and deletes Follow-up Date(Y) when column Survey: Intent to Participate = No
Dim lastrow As Long
Dim i As Long
Dim rRev As Range 'AJ
Dim rRole As Range 'U
Dim rFollowUp As Range 'Y
Dim rSurvey As Range 'AE
With Worksheets("Tracking")
Set rRev = .Rows(3).Find(What:="REV", MatchCase:=False, Lookat:=xlWhole)
Set rRole = .Rows(3).Find(What:="Role", MatchCase:=False, Lookat:=xlWhole)
Set rFollowUp = .Rows(3).Find(What:="Follow-up", MatchCase:=False, Lookat:=xlWhole)
Set rSurvey = .Rows(3).Find(What:="Survey", MatchCase:=False, Lookat:=xlWhole)
lastrow = rSurvey(.Rows.Count - rSurvey.Row).End(xlUp).Row - rSurvey.Row
End With
For i = 2 To lastrow
If Not IsError(rSurvey(i).value) Then
If rSurvey(i).value = "No" And rRole(i).value = "MEM" Then
rRole(i).value = "C-MEM"
rFollowUp(i).ClearContents
rRev(i).value = "N/A"
ElseIf rSurvey(i).value = "No" And rRole(i).value = "VCH" Then
rRole(i).value = "C-VCH"
rFollowUp(i).ClearContents
rRev(i).value = "N/A"
End If
End If
Next i
End Sub

excel vba - Using autofilter - can't pass the filtered range to a sub, it keeps passing the entire sheet range

I can't seem to figure this one out. I have a function and a sub where I call the function to get the unique values (from column N (text values)) from the range I've already selected from the autofilter. Somehow, the range keeps being the entire sheet range and not the selected.
Function UniquesFromRange(rng As Range)
Dim d As Object, c As Range, tmp
Set d = CreateObject("scripting.dictionary")
For Each c In rng.Cells
tmp = Trim(c.Value)
If Len(tmp) > 0 Then
If Not d.Exists(tmp) Then d.Add tmp, 1
End If
Next c
UniquesFromRange = d.Keys
End Function
Sub mainSub()
For Each key In fCatId.Keys
With wshcore
llastrow = wshcore.Range("A" & Rows.Count).End(xlUp).Row
.AutoFilterMode = False
.Range("A1:N" & llastrow).AutoFilter
.Range("A1:N" & llastrow).AutoFilter Field:=1, Criteria1:=fCatId(key)
lwmin = WorksheetFunction.Subtotal(5, Range("H:H"))
lwmax = WorksheetFunction.Subtotal(4, Range("H:H"))
'This does not work, I want to get the unique values from column N
'that are already in the filtered range. So far this shows
'all the values in the column not only the ones already filtered.
varArray = UniquesFromRange(Range("N:N"))
'I've also tried this:
'varArray = UniquesFromRange(Range.Cells)
'Debug.Print fCatId(key) & " - " & key & " " & lwmin & "-" & lwmax & fData(key) & " - " & Join(varArray, vbNewLine)
End With
Next key
Application.ScreenUpdating = True
End Sub
any suggestions?
Instead of
varArray = UniquesFromRange(Range("N:N"))
use
varArray = UniquesFromRange(Range("N1:N" & llastrow).SpecialCells(xlCellTypeVisible))
In response to the additional question asked in the comments, you could copy varArray to another sheet (assumed to already exist, and being referred to by the object wsOutput, and output to be written to column A) as follows
Dim r as Integer
For r = LBound(varArray) To UBound(varArray)
wsOutput.Cells(r, 1).Value = varArray(r)
Next

Proper `If Condition` and proper use of `Loop Structure` in Excel-VBA

My Workbook has three sheets, Namely; Questions, Answers and Incorrect Mappings.
In Questions Sheet:
Column A is Question_Id.
Column B: Answer_Type has value among : True/False, One another, Multi item, CheckBoxes, Event.
Column C: Answer_Id (One or More 'Numeric Values') separated with Semicolon.
In Answers Sheet:
Column A is Answer_Id.
(Few or all Answer IDs of Questions sheet will be listed here, each on a single row).
Column B is Frequency; which has values such as:
Event Based, Yearly, Half Yearly, Quarterly.
The Questions and Answers sheet are linked on Answer_Id column.
Requirement:
If any Question Id has 'Answer Types' such as True/False, One another, Multi item, CheckBoxes; then Answer Id's against it in
Answers sheet should not have frequency Event Based against such Answer_Id.
i.e. If Answer_Type is 'Event' then only, frequency against it should be Event Based
The incorrect mappings in Questions sheet should be sent to Incorrect Mappings sheet as hyperlinks to 'Questions' Sheet.
I've written the following code:
Dim shname, strstr, strErr, stString As String
Dim stArray() As String
Dim AnsIds1 As Range
Dim celadr, celval, AnsId1, AnsId2, questionType As Variant
Dim LastRow, LastRowSheet2 As Long
LastRow = Sheets("Questions").Cells(Rows.Count, 2).End(xlUp).Row
LastRowSheet2 = Sheets("Answers").Cells(Rows.Count, 2).End(xlUp).Row
For Each questionType In Worksheets("Questions").Range("B2:B" & LastRow)
celadr = questionType.Address
celval = questionType.Value
If Len(celval) >= 1 Then
If InStr(1, ("TRUE/FALSE,ONE ANOTHER,MULTI ITEM,CHECKBOXES,"), UCase(celval) & ",") >= 1 Then
For Each AnsIds1 In Worksheets("Questions").Range("C2:C" & LastRow)
stString = AnsIds1
stArray() = Split(stString, ";")
For Each AnsId1 In stArray()
For Each AnsId2 In Worksheets("Answers").Range("A2:A" & LastRowSheet2).Cells
If Trim(AnsId1) = Trim(AnsId2) Then
If Trim(UCase(AnsId2.Offset(0, 1).Value)) = "EVENT BASED" Then 'Is this If condition should be changed to something else?
AnsIds1.Interior.Color = vbRed
celadr = AnsIds1.Address
Sheets("Questions").Select
shname = ActiveSheet.Name
Sheets("Incorrect Mappings").Range("A65536").End(xlUp).Offset(1, 0).Value = AnsId2 & " Should not have Event based frequency"
strstr = "'" & shname & "'!" & Range(celadr).Address(0, 0)
Sheets("Incorrect Mappings").Hyperlinks.Add Anchor:=Sheets("Incorrect Mappings").Range("A65536").End(xlUp), Address:="", SubAddress:=strstr
End If
End If
Next
Next
Next
End If
End If
Next
When I run the above code, I do get the mixed output (an incorrect output).
After writing the code step by step and debugging it step by step, I feel the mistake is at line commented as
Is this If condition should be changed to something else? or at the line above it.
Can someone tell me, to what condition I've to change it?.
(Also, I need to change loop structure to get incorrect mappings only once in Incorrect Mappings sheet, but it's a second priority)
Your lookup to the keys on the Answers worksheet could be eased with the introduction of a Scripting.Dictionary object.
Sub question_Check_by_Dictionary()
Dim questionType As Range
Dim v As Long, vAIDs As Variant, d As Long, dict As Object
'load the dictionary with the answer types
Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = vbTextCompare
With Worksheets("Answers")
For d = 2 To .Cells(.Rows.Count, 1).End(xlUp).Row
dict.Item(CStr(.Cells(d, 1).Value2)) = .Cells(d, 2).Value2
Next d
End With
'reset the Questions worksheet
With Worksheets("Questions")
.Range(.Cells(2, 1), .Cells(.Rows.Count, 3).End(xlUp)).Interior.Pattern = xlNone
End With
'reset the Incorrect Mappings worksheet
With Worksheets("Incorrect Mappings")
.Range(.Cells(2, 1), .Cells(.Rows.Count, 1).End(xlUp)).Clear
End With
With Worksheets("Questions")
For Each questionType In .Range(.Cells(2, 2), .Cells(.Rows.Count, 2).End(xlUp))
If Not CBool(InStr(1, questionType.Value2, "event", vbTextCompare)) Then
vAIDs = Split(questionType.Offset(0, 1), Chr(59)) 'split on semi-colon
For v = LBound(vAIDs) To UBound(vAIDs)
If dict.exists(vAIDs(v)) Then
If CBool(InStr(1, dict.Item(CStr(vAIDs(v))), "event", vbTextCompare)) Then
questionType.Resize(1, 3).Offset(0, -1).Interior.Color = vbRed
With Sheets("Incorrect Mappings")
.Hyperlinks.Add Anchor:=.Range("A" & .Rows.Count).End(xlUp).Offset(1, 0), _
Address:="", SubAddress:=questionType.Address(external:=True), _
ScreenTip:="click to go to rogue question", _
TextToDisplay:="Question " & questionType.Offset(0, -1).Value2 & _
" should not have Event based frequency (" & _
vAIDs(v) & ")."
End With
End If
Else
questionType.Resize(1, 3).Offset(0, -1).Interior.Color = vbYellow
With Sheets("Incorrect Mappings")
.Hyperlinks.Add Anchor:=.Range("A" & .Rows.Count).End(xlUp).Offset(1, 0), _
Address:="", SubAddress:=questionType.Address(external:=True), _
ScreenTip:="click to go to rogue question", _
TextToDisplay:="Question " & questionType.Offset(0, -1).Value2 & _
" references an unknown Answer ID (" & _
vAIDs(v) & ")."
End With
End If
Next v
End If
Next questionType
End With
End Sub
I've added a check to ensure that the answer ID found in the Questions worksheet actually exists in the Answers worksheet.