Deleting entire columns based on column headers - excel

I am trying to delete all the columns in an Excel sheet except the columns with the headers of "Product code" "Size" and "Quantity".
I have written the following code.
Sub delcolumns()
Dim Rng As Range
Dim cell As Range
Set Rng = Range(("A1"), Range("A1").End(xlToRight))
For Each cell In Rng
If cell.Value <> "Product Code" Or "Size" Or "Quantity" Then cell.EntireColumn.Delete
Next cell
End Sub
After running the micro the error says "Type mismatch"

You should work backwards when deleting rows or columns or you risk skipping over one or more.
Sub delcolumns()
Dim c as long, cols as variant
cols = array("Product Code", "Size", "Quantity")
for c = cells(1, columns.count).end(xltoleft).column to 1 step -1
if iserror(application.match(cells(1, c).value, cols, 0)) then
columns(c).entirecolumn.delete
end if
next c
End Sub
'alternative
Sub delcolumns()
Dim c as long
for c = cells(1, columns.count).end(xltoleft).column to 1 step -1
select case cells(1, c).value
case "Product Code", "Size", "Quantity"
'do nothing
case else
columns(c).entirecolumn.delete
end select
next c
End Sub
As far as your own code code, there are a couple of problems.
If cell.Value <> "Product Code" Or "Size" Or "Quantity" Then cell.EntireColumn.Delete
The above line is improper syntax. Each criteria needs to be written out longhand.
If cell.Value <> "Product Code" Or cell.Value <> "Size" Or cell.Value <> "Quantity" Then cell.EntireColumn.Delete
See Is variable required instead of “or” for alternatives.
More importantly, your logic is flawed. If one column is Product Code, then it isn't Size or Quantity and it will get deleted. You actually want,
If cell.Value <> "Product Code" AND cell.Value <> "Size" AND cell.Value <> "Quantity" Then cell.EntireColumn.Delete
Using And instead of Or means that the column is none of the three then delete.

You won't have to delete backwards using this code. This method tends to be more efficient since the actions are outside of the loop.
Say that you have 20 columns and intend to delete 17 of them (keep your 3 columns that are needed). This means you will have 17 iterations of columns being deleted and rows being shifted.
Instead, keep track of your target columns to delete using Union (collection of cells) and then delete everything all at once outside of the loop. No matter how many columns you have to be deleted, you will always do it all in once instance rather n instances. The larger the number of columns to be deleted, the greater the gains from using this method.
Option Explicit
Sub DeleteMe()
Dim ws As Worksheet: Set ws = ThisWorkbook.Sheets("Sheet1") '<-- Update sheet
Dim LC As Long, MyHeader As Range, DeleteMe As Range
LC = ws.Cells(1, ws.Columns.Count).End(xlToLeft).Column
For Each MyHeader In ws.Range(ws.Cells(1, 1), ws.Cells(1, LC))
Select Case MyHeader
Case "Product code", "Size", "Quantity"
Case Else
If Not DeleteMe Is Nothing Then
Set DeleteMe = Union(DeleteMe, MyHeader)
Else
Set DeleteMe = MyHeader
End If
End Select
Next MyHeader
If Not DeleteMe Is Nothing Then DeleteMe.EntireColumn.Delete
End Sub

Write back a resized array without backward loops
In addition to the valid solutions above and in order to show an alternative approach using the advanced features of the
Application.Index function: all actions are executed within an array before writing it back to sheet.
Method
The Application.Index function allows not only to receive row and column numbers as arguments, but also row and column arrays with certain restructuring possibilities. The rows array contains the complete set of rows, the column array is built by a helper function getColNums() containing the related column numbers to the wanted titles "Product code", "Size" and "Quantity". - You might find some interesting pecularities of this function at Insert first column in datafield array without loops or API call.
Code example
This code example assumes a data range A1:F1000 which can be changed easily to your needs.
Sub RestructureColumns()
Dim rng As Range, titles(), v
titles = Array("Product code", "Size", "Quantity") ' << define wanted column titles
Set rng = ThisWorkbook.Worksheets("Sheet1").Range("A1:F1000") ' << change to wanted sheet and data range
' [1a] create 2-dim data field array (1-based)
v = rng.Value2
' [1b] filter out columns to be deleted, i.e. maintain the rest
v = Application.Index(v, Evaluate("row(1:" & rng.Rows.count & ")"), getColNums(v, titles))
' [2] write data field back to resized range
rng = "" ' clear lines
rng.Resize(UBound(v), UBound(v, 2)) = v ' write back only columns with predefined titles
End Sub
'Helper function getColNums()
Function getColNums(v, titles) As Variant()
' Purpose: return array of column numbers related to wanted titles, e.g. 1st, 3rd and 6th column
Dim tmpAr, title, foundCol, i& ' declare variables
ReDim tmpAr(0 To UBound(titles)) ' dimension array to titles length
For Each title In titles ' check the wanted titles only ...
foundCol = Application.Match(title, Application.Index(v, 1, 0), 0) ' ... against the complete title row
If Not IsError(foundCol) Then tmpAr(i) = foundCol: i = i + 1 ' if found add col no, increment counter
Next title
ReDim Preserve tmpAr(0 To i - 1) ' (redundant if all titles available)
getColNums = tmpAr ' return built array
End Function

Related

Concat several columns using column names in Excel VBA

As shown in image_1, I have the raw data of a product as shown in Column B to Column F. I want to add column A, which concats the "Model", "Year", "Number" data into a string. I know I can achieve this simply by
[a2] = "=concat(B2,D2,F2)", and then filldown. But the problem is that the raw file I receive every day is inconsistent in terms of the order of the columns. Therefore, I couldn't use a static line of code displayed above.
I can probably use a combination of for loop and if/else to test if the column name equal to "Model", "Year", "Number", and if yes, grab its column number...
However, I'm wondering if there's a more direct and elegant way of achieving this. Any thoughts?
A simple approach based on sorting columns by header and merging data in columns with constant numbers (assuming constant number of columns but different order). If the number of columns is variable, this code will not work.
Sub concat()
Dim rng As Range
With ThisWorkbook.Worksheets(1)
Set rng = .Range("A1").CurrentRegion
' columns sort
With .Sort
.SortFields.Clear
.SortFields.Add2 Key:=rng.Rows(1)
.SetRange rng
.Orientation = xlLeftToRight
.Apply
End With
Set rng = rng.Columns(1)
rng.Insert ' add cells at left for "Concat"
Set rng = rng.Offset(0, -1)
rng(1) = "Concat" ' add header
Intersect(rng, rng.Offset(1)).FormulaR1C1 = "=CONCAT(RC[2],RC[5],RC[3])"
End With
End Sub
Before
After
If you want to add the "Concat" column and formula without reordering the columns, you can do that with vba like this
Sub Demo()
Dim ws As Worksheet
Dim colModel As Variant
Dim colYear As Variant
Dim colNum As Variant
Dim LastRow As Long
Set ws = ActiveSheet ' or any means you choose
LastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
If ws.Cells(1, 1) <> "Concat" Then
'Insert new column
ws.Columns(1).Insert
' New column header
ws.Cells(1, 1) = "Concat"
End If
' get colum positions
colModel = Application.Match("Model", ws.Rows(1), 0)
colYear = Application.Match("Year", ws.Rows(1), 0)
colNum = Application.Match("Number", ws.Rows(1), 0)
' Check if columns exist
If IsError(colModel) Then
MsgBox "Column ""Model"" not found", vbCritical + vbOKOnly, "Error"
Exit Sub
End If
If IsError(colYear) Then
MsgBox "Column ""Year"" not found", vbCritical + vbOKOnly, "Error"
Exit Sub
End If
If IsError(colNum) Then
MsgBox "Column ""Number"" not found", vbCritical + vbOKOnly, "Error"
Exit Sub
End If
' Insert Formula
ws.Range(ws.Cells(2, 1), ws.Cells(LastRow, 1)).FormulaR1C1 = "=RC[" & colModel - 1 & "]&RC[" & colYear - 1 & "]&RC[" & colNum - 1 & "]"
End Sub
Alternatively, you could also use a formula in column A to find the column positions
In Excel365
=XLOOKUP("Model",$1:$1,2:2,,0)&XLOOKUP("Year",$1:$1,2:2,,0)&XLOOKUP("Number",$1:$1,2:2,,0)
For pre 365
=INDEX(2:2,MATCH("Model",$1:$1,0))&INDEX(2:2,MATCH("Year",$1:$1,0))&INDEX(2:2,MATCH("Number",$1:$1,0))
If they are always the same columns, just the order changes then sort by column headings first before concatenating, that way they will always be in the same position.
If you have differing columns and the ones you are interested in are somewhere within it, then you could use the following formula:
=HLOOKUP("Heading_Name","Data_Range",Row_No,FALSE) to extract each of the columns you are interested in. Concatenating the results of these would give you what you want and will work for any arrangement of columns and sizes of data providing you declare the range properly.

Excel IF Statement Limited

I am using an IF statement in Excel to search for portions of text in the previous column in order to assign a supplier and category to the expense.
Supplier Column
=IF(ISNUMBER(SEARCH("tit",[#Description])),"TITAN",IF(ISNUMBER(SEARCH("Sol",[#Description])),"Soltrack",IF(ISNUMBER(SEARCH("coin",[#Description])),"Coin",IF(ISNUMBER(SEARCH("gree",[#Description])),"Green Dream Projects",IF(ISNUMBER(SEARCH("sars V",[#Description])),"SARS VAT",IF(ISNUMBER(SEARCH("sars p",[#Description])),"SARS PAYE",IF(ISNUMBER(SEARCH("acb",[#Description])),"Debit Order","")))))))
Category Column
the next column then has the following to get the category of the supplier
=IF(ISNUMBER(SEARCH("TITAN",[#Payee])),"Direct Operating Cost",IF(ISNUMBER(SEARCH("Soltrack",[#Payee])),"Direct Operating Cost",IF(ISNUMBER(SEARCH("Coin",[#Payee])),"Direct Operating Cost",IF(ISNUMBER(SEARCH("Green Dream Projects",[#Payee])),"Direct Operating Cost",IF(ISNUMBER(SEARCH("SARS VAT",[#Payee])),"VAT",IF(ISNUMBER(SEARCH("SARS PAYE",[#Payee])),"PAYE",IF(ISNUMBER(SEARCH("Debit Order",[#Payee])),"Debit Order","")))))))
this is working great, but seems i have reached the limit (7) of IF statements I can use in one formula?
I have created the below function to search for text "tit" and if it matches it updates the Payee column.
'excel if range of cells contains specific text vba
Sub MacroToCheckIfRangeOfCellsContainsSpecificText_vba()
Set Rng = Range("B2:B572") ' You can change this
specificText = "*tit*" ' You can change this
For Each Cell In Rng.Cells
If UCase(Cell.Value) Like "*" & UCase(specificText) & "*" Then
Cell.Offset(0, 1) = "Titan"
Else
Cell.Offset(0, 1) = ""
End If
Next
End Sub
Would I need to create a new specificText = "*tit*" for each of the keywords and also a whole section for each of the "For Each" functions?
Dictionary Solution
The first idea is to use a dictionary Replacements and add all the serach/replace pairs there. This has one huge disadvantage. It is against the good practice to not mix logic (code) and data. Good practice would be to put the data not into the code but into a worksheet (see next solution).
Option Explicit
Public Sub MacroToCheckIfRangeOfCellsContainsSpecificText_vba()
Dim RngToCheck As Range
Set RngToCheck = ThisWorkbook.Worksheets("Sheet1").Range("B2:B572") ' specify in which workbook and worksheet
Dim Replacements As Object
Set Replacements = CreateObject("Scripting.Dictionary")
With Replacements
.Add "tit", "Titan"
.Add "sol", "Soltrack"
'add more here
End With
Dim InputValues() As Variant
InputValues = RngToCheck.Value 'read input values into array
Dim OutputValues() As Variant 'create an output array (same size as RngToCheck)
ReDim OutputValues(1 To RngToCheck.Rows.Count, 1 To 1)
Dim iRow As Long
For iRow = 1 To UBound(OutputValues, 1)
Dim Key As Variant
For Each Key In Replacements.Keys
If UCase(InputValues(iRow, 1)) Like "*" & UCase(Key) & "*" Then
OutputValues(iRow, 1) = Replacements(Key)
Exit For 'we don't need to test for the others if we found a key
End If
Next Key
Next iRow
'write output values from array next to input values in the cells
RngToCheck.Offset(ColumnOffset:=1).Value = OutputValues
End Sub
Worksheet Solution
The better solution would be to create a new worksheet Replacements as below:
This can easily be edited by anyone and you don't need to fiddle with the code later if you want to delete or add pairs.
Public Sub ImprovedCheckUsingWorksheet()
Dim RngToCheck As Range
Set RngToCheck = ThisWorkbook.Worksheets("Sheet1").Range("B2:B572") ' specify in which workbook and worksheet
Dim Replacements() As Variant 'read replacements from worksheet
Replacements = ThisWorkbook.Worksheets("Replacements").Range("A2", ThisWorkbook.Worksheets("Replacements").Cells(Rows.Count, "B").End(xlUp)).Value 'read input values into array
Dim InputValues() As Variant
InputValues = RngToCheck.Value 'read input values into array
Dim OutputValues() As Variant 'create an output array (same size as RngToCheck)
ReDim OutputValues(1 To RngToCheck.Rows.Count, 1 To 1)
Dim iRow As Long
For iRow = 1 To UBound(OutputValues, 1)
Dim rRow As Long
For rRow = 1 To UBound(Replacements, 1)
If UCase(InputValues(iRow, 1)) Like "*" & UCase(Replacements(rRow, 1)) & "*" Then
OutputValues(iRow, 1) = Replacements(rRow, 2)
Exit For 'we don't need to test for the others if we found a key
End If
Next rRow
Next iRow
'write output values from array next to input values in the cells
RngToCheck.Offset(ColumnOffset:=1).Value = OutputValues
End Sub
For a 3ʳᵈ column in your replacements worksheet you would need to adjust the following line to be until column "C":
Replacements = ThisWorkbook.Worksheets("Replacements").Range("A2", ThisWorkbook.Worksheets("Replacements").Cells(Rows.Count, "C").End(xlUp)).Value 'read input values into array
and the output values need another column too (second parameter needs to go 1 To 2):
ReDim OutputValues(1 To RngToCheck.Rows.Count, 1 To UBound(Replacements, 2) - 1) 'this works for any amount of columns as it reads the column count from the `Replacements`
the data needs to be written
OutputValues(iRow, 1) = Replacements(rRow, 2) 'first output column
OutputValues(iRow, 2) = Replacements(rRow, 3) 'second output column
and writing the output values needs to be adjusted too:
RngToCheck.Offset(ColumnOffset:=1).Resize(ColumnSize:=UBound(OutputValues, 2)).Value = OutputValues 'this works for any amount of columns as it reads the column count from `OutputValues`
Formula Solution
But if you have your data in a worksheet Replacements like above, and you don't rely an a partial match. Then you don't need VBA and can easily use a formula instead to look it up:
=IFERROR(INDEX(Replacements!B:B,MATCH(B:B,Replacements!A:A,0)),"")

Using vba to find column headers and adding a new record under that header

I am trying to create something that is capable of taking the value from one text box, searching a group of column headers to find the correct one, and then placing a new value from a second text box into the last row under that column. I adapted this code that I found on here, https://stackoverflow.com/a/37687346/13073514, but I need some help. This code posts the value from the second text box under every header, and I would like it to only post it under the header that is found in textbox 1. Can anyone help me and explain how I can make this work? I am new to vba, so any explanations would be greatly appreciated.
Public Sub FindAndConvert()
Dim i As Integer
Dim lastRow As Long
Dim myRng As Range
Dim mycell As Range
Dim MyColl As Collection
Dim myIterator As Variant
Set MyColl = New Collection
MyColl.Add "Craig"
MyColl.Add "Ed"
lastRow = ActiveSheet.Cells.Find("*", SearchOrder:=xlByRows, SearchDirection:=xlPrevious).Row
For i = 1 To 25
For Each myIterator In MyColl
If Cells(1, i) = myIterator Then
Set myRng = Range(Cells(2, i), Cells(lastRow, i))
For Each mycell In myRng
mycell.Value = Val(mycell.Value)
Next
End If
Next
Next
End Sub
Basic example:
Sub tester()
AddUnderHeader txtHeader.Text, txtContent.Text
End Sub
'Find header 'theHeader' in row1 and add value 'theValue' below it,
' in the first empty cell
Sub AddUnderHeader(theHeader, theValue)
Dim m
With ThisWorkbook.Sheets("Data")
m = Application.Match(theHeader, .Rows(1), 0)
If Not IsError(m) Then
'got a match: m = column number
.Cells(.Rows.Count, m).End(xlUp).Offset(1, 0).Value = theValue
Else
'no match - warn user
MsgBox "Header '" & theHeader & "' not found!", vbExclamation
End If
End With
End Sub
I have commented your code for your better understanding. Here it is.
Public Sub FindAndConvert()
Dim i As Integer
Dim lastRow As Long
Dim myRng As Range
Dim myCell As Range
Dim MyColl As Collection
Dim myIterator As Variant
Set MyColl = New Collection
MyColl.Add "Craig"
MyColl.Add "Ed"
Debug.Print MyColl(1), MyColl(2) ' see output in the Immediate Window
' your code starts in the top left corner of the sheet,
' moves backward (xlPrevious) from there by rows (xlByRows) until
' it finds the first non-empty cell and returns its row number.
' This cell is likely to be in column A.
lastRow = ActiveSheet.Cells.Find("*", SearchOrder:=xlByRows, SearchDirection:=xlPrevious).Row
For i = 1 To 25 ' do the following 25 times
' in Cells(1, i), i represents a column number.
' 1 is the row. It never changes.
' Therefore the code will look at A1, B1, C1 .. until Y1 = cells(1, 25)
For Each myIterator In MyColl ' take each item in MyColl in turn
If Cells(1, i) = myIterator Then
' set a range in the column defined by the current value of i
' extend it from row 2 to the lastRow
Set myRng = Range(Cells(2, i), Cells(lastRow, i))
' loop through all the cells in myRng
For Each myCell In myRng
' convert the value found in each cell to a number.
' in this process any non-numeric cells would become zero.
myCell.Value = Val(myCell.Value)
Next myCell
End If
Next myIterator
Next i
End Sub
As you see, there is no TextBox involved anywhere. Therefore your question can't be readily understood. However, my explanations may enable you to modify it nevertheless. It's all a question of identifying cells in the worksheet by their coordinates and assigning the correct value to them.
Edit/Preamble
Sorry, didn't read that you want to use TextBoxes and to collect data one by one instead of applying a procedure to a whole data range.
Nevertheless I don't remove the following code, as some readers might find my approach helpful or want to study a rather unknown use of the Application.Match() function :)
Find all header columns via single Match()
This (late) approach assumes a two-column data range (header-id and connected value).
It demonstrates a method how to find all existant header columns by executing a single Application.Match() in a ►one liner ~> see step [3].
Additional feature: If there are ids that can't be found in existant headers the ItemCols array receives an Error items; step [4] checks possible error items adding these values to the last column.
The other steps use help functions as listed below.
[1] getDataRange() gets range data assigning them to variant data array
[2] HeaderSheet() get headers as 1-based "flat" array and sets target sheet
[3] see explanation above
[4] nxtRow() gets next free row in target sheet before writing to found column
Example call
Sub AddDataToHeaderColumn()
'[1] get range data assigning them to variant data array
Dim rng As Range, data
Set rng = getDataRange(Sheet1, data) ' << change to data sheet's Code(Name)
'[2] get headers as 1-based "flat" array
Dim targetSheet As Worksheet, headers
Set targetSheet = HeaderSheet(Sheet2, headers)
'[3] match header column numbers (writing results to array ItemCols as one liner)
Dim ids: ids = Application.Transpose(Application.Index(data, 0, 1))
Dim ItemCols: ItemCols = Application.Match(ids, Array(headers), 0)
'[4] write data to found column number col
Dim i As Long, col As Long
For i = 1 To UBound(ItemCols)
'a) get column number (or get last header column if not found)
col = IIf(IsError(ItemCols(i)), UBound(headers), ItemCols(i))
'b) write to target cells in found columns
targetSheet.Cells(nxtRow(targetSheet, col), col) = data(i, 2)
Next i
End Sub
Help functions
I transferred parts of the main procedure to some function calls for better readibility and as possible help to users by demonstrating some implicit ByRef arguments such as [ByRef]mySheet or passing an empty array such as data or headers.
'[1]
Function getDataRange(mySheet As Worksheet, data) As Range
'Purpose: assign current column A:B values to referenced data array
'Note: edit/corrected assumed data range in columns A:B
With mySheet
Set getDataRange = .Range("A2:B" & .Cells(.Rows.Count, "B").End(xlUp).Row)
data = getDataRange ' assign range data to referenced data array
End With
End Function
'[2]
Function HeaderSheet(mySheet As Worksheet, headers) As Worksheet
'Purpose: assign titles to referenced headers array and return worksheet reference
'Note: assumes titles in row 1
With mySheet
Dim lastCol As Long: lastCol = .Cells(1, .Columns.Count).End(xlToLeft).Column
headers = Application.Transpose(Application.Transpose(.Range("A1").Resize(1, lastCol)))
End With
Set HeaderSheet = mySheet
End Function
'[4]
Function nxtRow(mySheet As Worksheet, ByVal currCol As Long) As Long
'Purpose: get next empty row in currently found header column
With mySheet
nxtRow = .Cells(.Rows.Count, currCol).End(xlUp).Row + 1
End With
End Function

How to compare and show differences on multiple columns in Microsoft Excel

I have a sheet with data in 2 columns, A and B:
--A-- --B--
Apple 57
Orange 62
Lime 45
Orange 58
Apple 57
What I want is, I need to search column A for duplicates, then if there are any, look for their value in column B. If they are different, I want to color the cell in column A to red, show the other value of that entry in column C, and show a message on how many indifferences there are. Something like this:
--A-- --B-- --C--
Apple 57
Orange 62 58
Lime 45
Orange 58 62
Apple 57
Please help me with this, I know how to compare the value in one column, but then don't know how to search for additional value for them in the other column.
Since i am still at learning process it may not be the best solution but it seems it is working
```
' inoG
Sub Solution()
Dim rows As Integer
rows = Range("a1").End(xlDown).Row 'Getting total row number
Dim dt As Variant
dt = Range("a1:c" & rows) 'data into array '
'forward search
For i = 1 To rows
For j = i + 1 To rows
If dt(i, 1) = dt(j, 1) And dt(i, 2) <> dt(j, 2) Then
dt(i, 3) = dt(j, 2)
GoTo Continue1
End If
Next j
Continue1:
Next i
'backward search
For i = rows To 1 Step -1
For j = i - 1 To 1 Step -1
If dt(i, 1) = dt(j, 1) And dt(i, 2) <> dt(j, 2) Then
dt(i, 3) = dt(j, 2)
GoTo Continue2
End If
Next j
Continue2:
Next i
'filling row C and Highlighting
For i = 1 To rows
If Not IsEmpty(dt(i, 3)) Then
Cells(i, 3) = dt(i, 3)
Range("A" & i).Interior.ColorIndex = 3
End If
Next i
'Final Message
Dim totdif As Integer
totdif = WorksheetFunction.CountA(Range("C1:C1" & rows))
MsgBox totdif
End Sub
My following solution used a helper column to rank the values in Column B per item in Column A using COUNTIFS function. Then I used a pivot table to show the average value of each rank for each item.
Presume you have following named ranges:
ListItem being your data in Column A;
ListValue being your data in Column B.
The formula in Cell C2 is:
=IF(COUNTIFS(ListItem,A2,ListValue,">"&B2)+1>1,"2nd Value","1st Value")
Change the cell references used to suit your case.
This solution will create an output table laying out all the unique items and then populate the two different values (if there are two) in two consecutive columns next to each item. For comparison purpose I think a pivot table is sufficient and quite efficient.
P.s. to create a pivot table, you just need to highlight the source table, go to Insert tab, and click the Pivot Table button to generate a pivot table. Set up the fields in the following way and you will have something similar to my example:
EDIT #2
If you want to show the second value in Column C for each item, here is a formula based approach.
In Cell C2 enter the following formula and drag it down:
=IFERROR(AGGREGATE(14,6,AGGREGATE(14,6,ListValue/(ListItem=A2),ROW($Z$1:INDEX($Z:$Z,COUNTIF(ListItem,A2))))/((AGGREGATE(14,6,ListValue/(ListItem=A2),ROW($Z$1:INDEX($Z:$Z,COUNTIF(ListItem,A2))))<>B2)),1),"")
The logic is to use ListValue/(ListItem=A2) to return a range of values for each item, then use AGGREGATE function to filter out all the errors, then use
AGGREGATE(14,6,ListValue/(ListItem=A2),ROW($Z$1:INDEX($Z:$Z,COUNTIF(ListItem,A2))))<>B2
to further filter the range to show the second value (which is different to the first value), then use AGGREGATE function again to return that value.
Let me know if you have any questions. Cheers :)
I think you can do this with formulas.
If you are concerned about users changing the formula, use a Table (and perhaps even protect the formula column, although this would require VBA code to allow expanding the table). That way the ranges will dynamically adjust to additions and deletions of data, and the users will not need to edit the formula:
With the table renamed Fruits, and the columns named as in the screenshot:
=IFERROR(AGGREGATE(14,6,1/(([#Fruit]=[Fruit])*([#Value]<>[Value]))*[Value],1),"")
Use Conditional Formatting to format the cells
EDIT:
I think the table approach would give you a better solution, but for a VBA approach I would use a Dictionary and a collection of the different values associated with the fruits.
Assuming your first column is named "Fruit" (or something you can use in Find, or even a known address), you can use the following to create a column of the alternate values for each item.
'Add reference to Microsoft Scripting Runtime
' or use late binding
Option Explicit
Sub diffs()
Dim myD As Dictionary
Dim vData As Variant
Dim rData As Range, C As Range
Dim wsSrc As Worksheet
Dim I As Long, V As Variant
Dim colVals As Collection
'Find the table
Set wsSrc = Worksheets("sheet2") 'or wherever
With wsSrc.Cells
Set C = .Find(what:="Fruit", after:=.Item(1, 1), LookIn:=xlValues, lookat:=xlWhole, MatchCase:=True)
If Not C Is Nothing Then
With wsSrc
Set rData = .Range(C, .Cells(.Rows.Count, C.Column).End(xlUp)).Resize(columnsize:=3)
vData = rData
End With
Else
MsgBox "No data table"
Exit Sub
End If
End With
'Collect the data into a dictionary
'Max 2 different values per fruit
Set myD = New Dictionary
myD.CompareMode = TextCompare
For I = 2 To UBound(vData)
If Not myD.Exists(vData(I, 1)) Then
Set colVals = New Collection
colVals.Add Item:=vData(I, 2), Key:=CStr(vData(I, 2))
myD.Add Key:=vData(I, 1), Item:=colVals
Else
On Error Resume Next 'omit duplicate values
myD(vData(I, 1)).Add Item:=vData(I, 2), Key:=CStr(vData(I, 2))
On Error GoTo 0
End If
Next I
'Populate column 3
For I = 2 To UBound(vData, 1)
Set colVals = myD(vData(I, 1))
vData(I, 3) = ""
If colVals.Count > 1 Then
For Each V In colVals
If V <> vData(I, 2) Then vData(I, 3) = V
Next V
End If
Next I
Application.ScreenUpdating = False
With rData
.Clear
.Value = vData
For I = 2 To UBound(vData)
If vData(I, 3) <> "" Then
With rData.Cells(I, 1)
.Font.Color = vbWhite
.Font.Bold = True
.Interior.Color = vbRed
End With
End If
Next I
End With
End Sub

Match function in excel macro giving only first result

i'm new to excal macros/vba, and i am encountering a problem which i do not know how to approach.
I have a workbook that includes several sheets. There is 1 file which is more or less a master list, and 3 files which are sort of a packing list.
I have put in a command button with a macro in the 3 packing list respectively that tells me if a certain item in the packing list exist in the master, and if it does it tells me which row it appears in. This is working fine, however my problem is that if a particular items appears several times in the master list(due to different purchase date), the macro only gives the first result.
I would like to know if there are any ways such that all possible results appears instead of just the first.
below is a sample of the code i used
Private Sub CommandButton1_Click()
Dim k As Integer
For k = 3 To 1000
Cells(k, 24).Value = Application.Match(Cells(k, 2), Sheets("master").Range("B2:B1000"), 0)
Next k
End Sub
if your "master" sheet data is a list of contiguous not empty cells from B2 down to last not empty one, then here's a different approach playing around a bit with
Option Explicit
Private Sub CommandButton1_Click()
Dim cell As Range
With Worksheets("master") ' reference your "master" sheet
With .Range("B2", .Cells(.Rows.Count, "B").End(xlUp)) ' reference referenced sheet column B range from row 2 down to last not empty one
For Each cell In Range("B3", Cells(Rows.Count, "B").End(xlUp)) ' loop through packinglist sheet (i.e. where button resides) column B cells from row 3 down to last not empty one
If Not .Find(what:=cell.Value2, LookIn:=xlValues, lookat:=xlWhole) Is Nothing Then ' if current packinglist item is in "master"
.Replace what:=cell.Value2, replacement:=vbNullString, lookat:=xlWhole ' temporarily replace master item with a blank
cell.Offset(, 22).Value2 = Replace(.SpecialCells(xlCellTypeBlanks).Address(False, False), "B", "") ' write master list blanks rows in packinglist sheet current item row and column "X"
.SpecialCells(xlCellTypeBlanks).Value = cell.Value2 ' restore master list current packinglist item value
End If
Next
End With
End With
End Sub
I would use a dictionary to store every item in the master sheet, and everytime you find it duplicate, add another number with its row like this:
Option Explicit
Private Sub CommandButton1_Click()
Dim MasterKeys As Object
MasterKeys = FillDictionary(MasterKeys)
With ThisWorkbook.Sheets("MySheet") 'change MySheet for your actual sheet name
Dim arr As Variant
arr = .UsedRange.Value 'drop your data inside an array
Dim i As Long
For i = 3 To UBound(arr) 'loop through all the rows in your data
If MasterKeys.Exists(arr(i, 2)) Then arr(i, 24) = MasterKeys(arr(i, 2))
Next i
.UsedRange.Value = arr 'drop back your data
End With
End Sub
Function FillDictionary(MasterKeys As Object) As Object
Set MasterKeys = CreateObject("Scripting.Dictionary")
With Workbooks("MasterWorkbook.xlsx").Sheets("master") 'change MasterWorkbook for the actual filename of your master workbook
Dim LastRow As Long
LastRow = .Cells(.Rows.Count, 2).End(xlUp).Row 'find the last row on column B
Dim C As Range
For Each C In .Range("B2:B" & LastRow) 'loop through the range
If Not MasterKeys.Exists(C.Value) Then
MasterKeys.Add C.Value, C.Row
Else
MasterKeys(C.Value) = MasterKeys(C.Value) & "," & C.Row
End If
Next C
End With
End Function

Resources