I need a VBA code that searches for a specific Name (first dropdown), Products (second drop-down), then returns the unit price. I can use VLOOKUP to search names and return the unit price but I need to search for names and products and be able to pull the prices quickly. I used Evaluate function but the result is #VALUE!
Sub unitPrice()
Set sh4 = ThisWorkbook.Sheets("Invoice")
Set sh5 = ThisWorkbook.Sheets("Unit Price")
sh4.Range("H18") = _
sh4.Evaluate("MATCH(" & sh4.Cells(11, 1).Address(False, False) _
& "&" & sh4.Cells(18, 1).Address(False, False) _
& ",'Sh5!B2:B5&sh5!A2:A5,0)")
End Sub
Screenshot of Invoice and Unit Price sheet
I am assuming that you have two tables (insert > table): tblInvoice and tblUnitPrice. It is much easier to reference them in VBA via listobject than without. If you are not using tables you have to adjust the ranges accordingly.
What my code does: It inserts an INDEX/MATCH-Formula to retrieve the Unitprice for all rows in the table - and then writes the pure values back to the cells.
Public Sub updateUnitPricesInInvoice()
Dim loInvoice As ListObject
Set loInvoice = ThisWorkbook.Worksheets("Invoice").ListObjects("tblInvoice")
With loInvoice.ListColumns("UnitPrice").DataBodyRange
.Formula2 = "=INDEX(tblUnitPrices[UnitPrice],MATCH(1,(tblUnitPrices[Name]=[#Name])*(tblUnitPrices[Product]=[#Product])))"
.value = .value
End With
End Sub
Alternative solution minimising interaction with sheet by matching in memory:
Option Explicit
Sub SimpleMatch()
Dim sh5 As Worksheet, sh4 As Worksheet 'declare vars
Set sh4 = ThisWorkbook.Sheets("Invoice") 'set sheet
Set sh5 = ThisWorkbook.Sheets("Unit Price") 'set sheet
Dim arr, arr2, LastRowSh4 As Long, LastRowSh5 As Long
LastRowSh4 = sh4.Cells(sh4.Rows.Count, "A").End(xlUp).Row 'count rows from last row
LastRowSh5 = sh5.Cells(sh5.Rows.Count, "A").End(xlUp).Row 'count rows from last row
arr = sh4.Range(sh4.Cells(1, 1), sh4.Cells(LastRowSh4, 8)).Value2 'load invoices to mem
arr2 = sh5.Range(sh5.Cells(1, 1), sh5.Cells(LastRowSh5, 3)).Value2 'load prices to mem
Dim j As Long, dict As Object
Set dict = CreateObject("Scripting.Dictionary") 'create dictionary lateB
With dict 'used because I'm to lazy to retype dict everywhere :)
.CompareMode = 1 'textcompare
For j = 1 To UBound(arr2) 'add prices to dict
If Not .Exists(arr2(j, 1) & arr2(j, 2)) Then 'set key if I don't have it yet in dict
.Add Key:=arr2(j, 1) & arr2(j, 2), Item:=arr2(j, 3)
End If
Next j
Dim cust As String
For j = 1 To UBound(arr)
If arr(j, 1) = "Bill To:" Then
cust = arr(j + 1, 1) 'assumes you have only 1 customer in the sheet!
End If
If .Exists(arr(j, 1) & cust) Then 'retrieve produc & cust price
arr(j, 8) = dict(arr(j, 1) & cust) 'add to arr
End If
Next j
End With
With sh4
.Range(.Cells(1, 1), .Cells(UBound(arr), UBound(arr, 2))) = arr 'dump updated array to invoice sheet
End With
End Sub
This is the solution without tables/listobjects:
Assumption: you have added names for the following cells on invoice sheet
A11: customer
A17: labelDescription
H17: labelUnitPrice
H28: labelTotalAmount
In the first step we retrieve the range between the two labels "UnitPrice" and "TotalAmount" - that's where the formula goes.
Then the formula is written to that range - using again INDEX/MATCH.
In case there is not description nothing is displayed (there ISERROR)
And again: after calculation formulas are replaced by their values
Option Explicit
Public Sub updateUnitPricesInInvoice()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Invoice")
Dim rgUnitPrices As Range
Set rgUnitPrices = getRangeBetweenTwoLabels(ws, "labelUnitPrice", "labelTotalAmount")
With rgUnitPrices
'Excel 365
'.Formula2 = "=IFERROR(INDEX(UnitPrice!C:C,MATCH(1,(UnitPrice!A:A=Invoice!" & ws.Range("labelDescription").Offset(1).Address(False, True) & ")*(UnitPrice!B:B=customer),0)),"""")"
'other Excel versions
With rgUnitPrices
.Formula = "=IFERROR(INDEX(UnitPrice!C:C,MATCH(1,(UnitPrice!A:A=Invoice!$A" & rgUnitPrices.Rows(1).Row & ")*(UnitPrice!B:B=customer),0)),"""")"
.FormulaArray = .FormulaR1C1
End With
.Value = .Value
End With
End Sub
Private Function getRangeBetweenTwoLabels(ws As Worksheet, _
label1 As String, label2 As String)
Dim cStart As Range: Set cStart = ws.Range(label1).Offset(1)
Dim cEnd As Range: Set cEnd = ws.Range(label2).Offset(-1)
Set getRangeBetweenTwoLabels = ws.Range(cStart, cEnd)
End Function
Related
I cobbled together something that does work for me as is, but it runs very slowly and I'm sure the code can be simplified.
Sub CopyPasteValues()
Dim strSht1, strSht2 As String
Dim c, rng As Range
strSht1 = "Edit"
strSht2 = "LOB"
With ThisWorkbook.Sheets(strSht1)
Set rng = Range("J2:AJ37")
For Each c In rng
If Not c.Value = 0 Then
Cells(c.Row, 2).Copy
ThisWorkbook.Sheets(strSht2).Range("B" & Rows.Count).End(xlUp).Offset(1).PasteSpecial xlPasteValues
Range(Cells(c.Row, 4), Cells(c.Row, 5)).Copy
ThisWorkbook.Sheets(strSht2).Range("C" & Rows.Count).End(xlUp).Offset(1).PasteSpecial xlPasteValues
c.Copy
ThisWorkbook.Sheets(strSht2).Range("F" & Rows.Count).End(xlUp).Offset(1).PasteSpecial xlPasteValues
Cells(c.Column).Copy
ThisWorkbook.Sheets(strSht2).Range("G" & Rows.Count).End(xlUp).Offset(1).PasteSpecial xlPasteValues
End If
Next c
End With
End Sub
I appreciate any assistance.
As BigBen Mentioned, array method.
Super Fast.
Sub Move_Values_Array_Method()
Dim SourceSheet As Worksheet 'Source Worksheet
Dim DestinationSheet As Worksheet 'Destination Worksheet
Dim RG As Range 'Source Range
Dim InArr() 'Data In Array
Dim OutArr() 'Data Out Array
Dim X As Long 'Array X Position for purposes of iterating through array.
Dim Y As Long 'Array Y Position for purposes of iterating through array.
Dim Cnt As Long 'Found Value Count
Set SourceSheet = ThisWorkbook.Worksheets("Edit") 'Set Source Worksheet
Set DestinationSheet = ThisWorkbook.Worksheets("LOB") 'Set Dest Worksheet
Set RG = SourceSheet.Range("J2:AJ37") 'Set Source Range
ReDim OutArr(1 To RG.Cells.Count) 'Count Cells in Range, resize output array to be at least that big.
InArr = RG 'Transfer Range Data to Array
Cnt = 0
Debug.Print LBound(InArr, 1) & " - " & UBound(InArr, 1) 'Rows
Debug.Print LBound(InArr, 2) & " - " & UBound(InArr, 2) 'Columns
For Y = 1 To UBound(InArr, 1) 'For Each Row in Array (or each Y position)
For X = 1 To UBound(InArr, 2) 'For Each Column in Array (or each X position)
If InArr(Y, X) <> "" Then 'If not blank Value (you can change this to "If InArr(Y, X) <> 0 Then" if that works best for you.
Cnt = Cnt + 1 'Increment "found value count" by 1
OutArr(Cnt) = InArr(Y, X) 'Add found value to output array
End If
Next X
Next Y
'Output to Dest Sheet
DestinationSheet.Range("F2").Resize(UBound(OutArr, 1), 1).Value = Application.Transpose(OutArr())
End Sub
Based on the information in your previous comments, try these alternative solution using formulas and filters...
1) Array Formulas
To note:
I have put everything on one sheet for clarity, but it works just as well over multiple sheets, or even workbooks.
If you want to filter the entire sheet, with same column order, you only need to enter formula once and expand "Array" criteria in formula to encapsulate entire data set.
Formula used in cell "J4" = "=FILTER($I$4:$I$30,$C$4:$C$30>0)"
(filter range I4 to I30 to show rows where value in range C4 to C30 is greater than 0)
2) Directly Filter
Alternatively, you could (either manually or programmatically) copy all data to LOB sheet, (or selectively copy), then filter for Qty>0.
I need the expert help in VBA as I am new. Actually I am looking for Vba code for Consecutive Count on the bases of two column (Serial Number and Alert Code) on button click event. The Column row are not fixed (dynamically change). The Consecutive count is maximum repeat count for Alert Code per Serial number. This should display in output worksheet as per max repeat Alert count per Serial number
Input Worksheet:
Expected Output :
The repeat count work as below pattern from Input sheet (Just for reference only).
Mine source code as below but this does not reference the 1st Column Serial Number (This only work for One column like AlertCode) :
Sub ConsecutiveCount()
Dim lr As Long, c As Range, a As Long
Application.ScreenUpdating = False
lr = Worksheets("Count2").Cells(Rows.Count, 1).End(xlUp).Row
For Each c In Range("B2:B" & lr)
If c.Value <> c.Offset(1).Value Then
a = Cells(c.Row, 3).End(xlUp).Row
' Range(Cells(c.Row, 4), Cells(c.Row, 4).End(xlUp).Offset(1)).Value = c.Row - a
Cells(c.Row, 3).Value = c.Row - a
Else
End If
Next c
Application.ScreenUpdating = True
End Sub
Current Output (Serial number not included)
Screenshot(s) / here(♪) refers:
Named ranges/setup
First, define a couple of named ranges to assist with referencing / formulating in VBA:
Name: range_data: dynamic range that references the two columns of interest (here, col 1&2 in Sheet1):
Refers to: =Sheet1!$D$3:OFFSET(Sheet1!$E$3,COUNTA(Sheet1!$E$3:$E$99995)-1,0,1,1)
Name: range_summary_startcell: a static range that references the desired upper-left cell of the output table / summary.
Refers to: =Sheet1!$G$3
The summary table itself shall comprise a number of rows (depending upon range_data) and 3 columns (given the input/Q) - this will be produced by the macro (code below) and can be seen in screenshot above (G3:I5) - the macro functions shall determine the appropriate dimensions automatically
Code
With these two named ranges (i.e. 'range_data' & 'range_summary_startcell') defined, the following VB code produces the desired output per your Q:
Sub Macro_Summary()
'
'JB_007 07/01/2022
'
'
Application.ScreenUpdating = True
Range("range_summary_startcell").Select
ActiveCell.Formula2R1C1 = "=UNIQUE(range_data)"
ActiveSheet.Calculate
x = ActiveCell.End(xlDown).Row
Set range_count = ActiveCell.Offset(0, 2)
range_count.Select
range_count.Formula2R1C1 = _
"=COUNTIFS(INDEX(range_data,0,2),RC[-1],INDEX(range_data,0,1),RC[-2])"
Selection.AutoFill Destination:=Range(range_count, range_count.Offset(x - range_count.Row))
ActiveSheet.Calculate
End Sub
Caveats: assumes you have Office 365 compatible version of Excel
GIF - Running Macro
Notes (♪) saved as macro-free workbook for your own security if you wish to download underlying workbook - otherwise identical to screenshot(s) in this proposed soln.
Sub ConsecutiveCount()
Dim srcLastRow As Long, cntConsec As Long, i As Long
Dim rng As Range
Dim srcArr() As Variant
Dim srcSht As Worksheet
Dim destsht As Worksheet
Dim destArr() As Variant
Dim combID As String
Dim splitID As Variant
Application.ScreenUpdating = False
Set srcSht = Worksheets("Input")
Set destsht = Worksheets("Output")
With srcSht
srcLastRow = .Cells(.Rows.Count, "A").End(xlUp).Row + 1 ' include 1 blank line
srcArr = .Range(.Cells(2, "A"), .Cells(srcLastRow, "B"))
End With
Dim dict As Object
Dim dKey As Variant
Set dict = CreateObject("Scripting.dictionary")
cntConsec = 0
For i = LBound(srcArr) To UBound(srcArr)
cntConsec = cntConsec + 1
If i <> UBound(srcArr) Then
If srcArr(i, 1) <> srcArr(i + 1, 1) Or srcArr(i, 2) <> srcArr(i + 1, 2) Then
combID = srcArr(i, 1) & "|" & srcArr(i, 2)
If dict.Exists(combID) Then
' check if sum is more
If dict(combID) < cntConsec Then ' If new max for combination
dict(combID) = cntConsec
End If
Else
' add to dictionary
dict(combID) = cntConsec
End If
cntConsec = 0
End If
End If
Next i
ReDim destArr(1 To dict.Count, 1 To 3)
i = 0
For Each dKey In dict.keys
splitID = Split(dKey, "|")
i = i + 1
destArr(i, 1) = splitID(0)
destArr(i, 2) = splitID(1)
destArr(i, 3) = dict(dKey)
Next dKey
destsht.Range("A2").Resize(UBound(destArr), 3).Value = destArr
Application.ScreenUpdating = True
End Sub
I got a sheet that contain weekly roster of each employee. The code below run perfectly to display unique data of one column:
Dim lastrow As Long
Application.ScreenUpdating = False
Dim rng, lastcell As Range
Set rng = Application.InputBox("Select a range", "Obtain Range Object", Type:=8)
lastrow = Cells(Rows.Count, "B").End(xlUp).Row
ActiveSheet.Range(rng.Address & lastrow).AdvancedFilter _
Action:=xlFilterCopy, _
CopyToRange:=ActiveSheet.Range(rng.Cells(rng.Rows.Count + 1, rng.Columns.Count).Address), _
Unique:=True
Application.ScreenUpdating = True
But my issue is that I want the code to exclude some text like OFF
and LEAVE. The only data to display is their shift which is in the format, 0430_1145 for timein_timeout in an asecending way.
The data normally is displayed at the end of each column:
If column have data such as:
0700_1500
0430_1145
leave
off
0700_1500
0830_1615
result would be(ascending way ignoring off and leave)-
0430_1145
0700_1500
0830_1615
Below is the link of my excel sheet:
https://drive.google.com/file/d/1CYGS9ZgsulG8J_qzYEUXWFiXkBHneibv/edit
If you have O365 with the appropriate functions, you can do this with a worksheet formula:
=SORT(UNIQUE(FILTER(A1:A6,(A1:A6<>"off")*(A1:A6<>"leave"))))
In the below image, the formula is entered into cell A8
Edit: Here is a VBA routine based on the worksheet you uploaded.
The result of the extraction of each column is stored as an ArrayList in a Dictionary.
I used an ArrayList because it is easy to sort -- but you could use any of a number of different objects to store this information, and write a separate sorting routine.
I also used late-binding for the dictionary and arraylist objects, but could switch that to early-binding if you have huge amounts of data to process and need the increased speed.
Note that the data is processed from a VBA array rather than on the worksheet.
many modifications are possible depending on your needs, but this should get you started.
Option Explicit
Sub summarizeShifts()
Dim wsSrc As Worksheet 'data sheet
Dim vSrc As Variant, vRes As Variant 'variant arrays for original data and results
Dim rRes As Range 'destination for results
Dim dShifts As Object ' store shifts for each day
Dim AL As Object 'store in AL to be able to sort
Dim I As Long, J As Long, S As String, V As Variant, W As Variant
'read source data into array
Set wsSrc = Worksheets("fnd_gfm_1292249")
With wsSrc
vSrc = .Range(.Cells(1, 1), .Cells(.Rows.Count, 1).End(xlUp)).Resize(columnsize:=9)
Set rRes = .Cells(UBound(vSrc, 1) + 1, 3) 'bottom of source data
End With
Set dShifts = CreateObject("Scripting.Dictionary")
'Populate the dictionary by columns
For J = 3 To UBound(vSrc, 2)
Set AL = CreateObject("System.Collections.ArrayList")
For I = 2 To UBound(vSrc, 1)
S = vSrc(I, J)
If S Like "####_####" Then
If Not AL.contains(S) Then AL.Add S
End If
Next I
AL.Sort
dShifts.Add J, AL
Next J
'size vres
I = 0
For Each V In dShifts
J = dShifts(V).Count
I = IIf(I > J, I, J)
Next V
ReDim vRes(1 To I, 1 To UBound(vSrc) - 2)
'populate results array
For Each V In dShifts
I = 0
For Each W In dShifts(V)
I = I + 1
vRes(I, V - 2) = W
Next W
Next V
'write the results
Application.ScreenUpdating = False
Set rRes = rRes.Resize(UBound(vRes, 1), UBound(vRes, 2))
With rRes
.Resize(rowsize:=rRes.Rows.Count * 3).ClearContents 'or something to clear rows below the data
.Value = vRes
End With
End Sub
Approach via FilterXML()
In addition to the valid solutions above I demonstrate an alternative solution via FilterXML() available since vers. 2013+:
Sub ExtractUniques20201019()
'a) define Worksheet
Dim ws As Worksheet: Set ws = Sheet1 ' << change to project's sheet Code(Name)
'b) get first target Row (2 rows below original data)
Dim tgtRow As Long: tgtRow = UBound(getData(ws, "A", 1)) + 2
Dim i As Long
For i = 3 To 9 ' columns C:I (Monday to Sunday)
'[1] get data
Dim data: data = getData(ws, i) ' << function call getData()
'[2] get valid unique data
Dim uniques: uniques = getFilterUniques(data) ' << function call getFilterUniques()
BubbleSortColumnArray uniques ' << call procedure BubbleSortColumnArray
'[3] write results to target below data range
ws.Range("A" & tgtRow).Offset(columnoffset:=i - 1).Resize(UBound(uniques), 1) = uniques
Next i
End Sub
Help functions
Function getData(ws As Worksheet, ByVal col, Optional ByVal StartRow& = 2) As Variant()
' Purpose: assign column data to variant array
If IsNumeric(col) Then col = Split(ws.Cells(1, col).Address, "$")(1)
Dim lastRow As Long
lastRow = ws.Range(col & Rows.Count).End(xlUp).Row
getData = ws.Range(col & StartRow & ":" & col & lastRow).Value2
End Function
Function getFilterUniques(arr, Optional Fltr As String = "_")
'Purpose: get unique items containing e.g. Fltr "_" using XPath search
'Note: WorksheetFunction.FilterXML() is available since vers. 2013+
' XPath examples c.f. https://stackoverflow.com/questions/61837696/excel-extract-substrings-from-string-using-filterxml/61837697#61837697
Dim content As String ' well formed xml content string
content = "<t><s>" & Join(Application.Transpose(arr), "</s><s>") & "</s></t>"
getFilterUniques = WorksheetFunction.FilterXML(content, "//s[not(preceding::*=.)][contains(., '" & Fltr & "')]")
End Function
Bubblesort
Sub BubbleSortColumnArray(arr, Optional ByVal ColNo As Long = 1)
'Purpose: sort 1-based 2-dim datafield array
'correct differing column index
Dim colIdx As Long: colIdx = LBound(arr) + ColNo - 1
'bubble sort
Dim cnt As Long, nxt As Long, temp
For cnt = LBound(arr) To UBound(arr) - 1
For nxt = cnt + 1 To UBound(arr)
If arr(cnt, colIdx) > arr(nxt, colIdx) Then
temp = arr(cnt, colIdx) ' remember element
arr(cnt, colIdx) = arr(nxt, colIdx) ' swap
arr(nxt, colIdx) = temp
End If
Next nxt
Next cnt
End Sub
Consider using the one argument of AdvancedFilter you do not use: CriteriaRange. This can allow you to set up a multiple set criteria that leaves out those values. See Microsoft's Filter by using advanced criteria tutorial doc section: Multiple sets of criteria, one column in all sets.
Essentially, this involves adding a new region outside of data region somewhere in worksheet or workbook with column headers and needed criteria which would be <>LEAVE AND <>OFF which as link above shows would require two same named columns for AND logic.
Criteria Region
A B C D E F G H I J K L M N
1 Monday Monday Tuesday Tuesday Wednesday Wednesday Thursday Thursday Friday Friday Saturday Saturday Sunday Sunday
2 <>LEAVE <>OFF <>LEAVE <>OFF <>LEAVE <>OFF <>LEAVE <>OFF <>LEAVE <>OFF <>LEAVE <>OFF <>LEAVE <>OFF
VBA
Below defines worksheet objects and avoids the use of ActiveSheet. See Two main reasons why .Select, .Activate, Selection, Activecell, Activesheet, Activeworkbook, etc. should be avoided.
...
Set data_ws = ThisWorkbook.Worksheets("myCurrentWorksheet")
Set criteria_ws = ThisWorkbook.Worksheets("myCurrentWorksheet")
data_ws.Range(rng.Address & lastrow).AdvancedFilter _
Action:=xlFilterCopy, _
CriteriaRange:=criteria_ws.Range("A1:N2")
CopyToRange:=data_ws.Range(rng.Cells(rng.Rows.Count + 1, rng.Columns.Count).Address), _
Unique:=True
I need a dynamic way to add Note in which cell in my ID column A. However the comments need to use the information from Column B and C. ex: ON 01/13/2020, Anne.
I am not sure how to check how many times each value from column A will appear and use information from column D and B to create the comment (NOTE)..
result I need. All the time the ID number will be the same the comments need to be the same as well.
The code I am using is
Sub Cmt_test()
Sheet1.Range("A2").AddComment "On " & Sheet1.Range("D2") & ", " & Sheet1.Range("B2")
End Sub
I don't know how I can make it dynamic to get the information all the time the same ID appears. Maybe if I use Loop on column A would it be possible that all the time the loop finds the same ID to add the comment using the information from column D and B?
Write Comments to Each Cell in a Column
Option Explicit
Sub addComments()
Const wsName As String = "Sheet1"
Const FirstRow As Long = 2
Const LastRowCol As Long = 1 ' or "A"
Const str1 As String = "On "
Const str2 As String = ", "
Dim Cols As Variant: Cols = Array(1, 2, 4)
Dim wb As Workbook: Set wb = ThisWorkbook
Dim ws As Worksheet: Set ws = wb.Worksheets(wsName)
Dim LastRow: LastRow = ws.Cells(ws.Rows.Count, LastRowCol).End(xlUp).Row
Dim Vals As Variant: ReDim Vals(UBound(Cols))
' Define Source Range.
Dim rng As Range: Set rng = ws.Range(ws.Cells(FirstRow, Cols(0)), _
ws.Cells(LastRow, Cols(0)))
' Write Column Ranges to Arrays.
Dim j As Long
For j = 0 To UBound(Cols)
Vals(j) = rng.Offset(, Cols(j) - Cols(0))
Next j
' Loop through elements (rows) of Source Array
' and write comments to a dictionary.
Dim dict As Object, Curr As Variant, i As Long
Set dict = CreateObject("Scripting.Dictionary")
For i = 1 To UBound(Vals(0))
Curr = Vals(0)(i, 1)
If dict(Curr) <> "" Then
dict(Curr) = dict(Curr) & vbLf & str1 _
& Format(Vals(2)(i, 1), "mm/dd/yyyy") & str2 & Vals(1)(i, 1)
Else
dict(Curr) = str1 _
& Format(Vals(2)(i, 1), "mm/dd/yyyy") & str2 & Vals(1)(i, 1)
End If
Next i
' Write comments from the dictionary to Source Range.
rng.ClearComments
Dim cel As Range
For Each cel In rng.Cells
cel.AddComment dict(cel.Value)
Next cel
End Sub
I've got three sheets - main,specimen and output in an excel workbook. The sheet main and speciment contain some information. Some of the information in two sheets are identical but few of them are not. My intention is to paste those information in output which are available in speciment but not in main.
I've tried like [currently it fills in lots of cells producing duplicates]:
Sub getData()
Dim cel As Range, celOne As Range, celTwo As Range
Dim ws As Worksheet: Set ws = ThisWorkbook.Worksheets("main")
Dim ws1 As Worksheet: Set ws1 = ThisWorkbook.Worksheets("specimen")
Dim ws2 As Worksheet: Set ws2 = ThisWorkbook.Worksheets("output")
For Each cel In ws.Range("A2:A" & ws.Cells(Rows.Count, 1).End(xlUp).row)
For Each celOne In ws1.Range("A2:A" & ws1.Cells(Rows.Count, 1).End(xlUp).row)
If cel(1, 1) <> celOne(1, 1) Then ws2.Range("A" & Rows.Count).End(xlUp).Offset(1, 0).value = celOne(1, 1)
Next celOne
Next cel
End Sub
main contains:
UNIQUE ID FIRST NAME LAST NAME
A0000477 RICHARD NOEL AARONS
A0001032 DON WILLIAM ABBOTT
A0290191 REINHARDT WESTER CARLSON
A0290284 RICHARD WARREN CARLSON
A0002029 RAYMOND MAX ABEL
A0002864 DARRYL SCOTT ABLING
A0003916 GEORGES YOUSSEF ACCAOUI
specimen contains:
UNIQUE ID FIRST NAME LAST NAME
A0288761 ROBERT HOWARD CARLISLE
A0290284 RICHARD WARREN CARLSON
A0290688 THOMAS A CARLSTROM
A0002029 RAYMOND MAX ABEL
A0002864 DARRYL SCOTT ABLING
output should contain [EXPECTED]:
UNIQUE ID FIRST NAME LAST NAME
A0288761 ROBERT HOWARD CARLISLE
A0290688 THOMAS A CARLSTROM
How can I achieve that?
If you have the latest version of Excel, with the FILTER function and dynamic arrays, you can do this with an Excel formula.
I changed your Main and Specimen data into tables.
On the Output worksheet you can then enter this formula into a single cell:
=FILTER(specTbl,ISNA(MATCH(specTbl[UNIQUE ID],mnTbl[UNIQUE ID],0)))
The remaining fields will autopopulate with the results.
For a VBA solution, I like to use Dictionaries, and VBA arrays for speed.
'set reference to microsoft scripting runtime
' or use late-binding
Option Explicit
Sub findMissing()
Dim wsMain As Worksheet, wsSpec As Worksheet, wsOut As Worksheet
Dim dN As Dictionary, dM As Dictionary
Dim vMain As Variant, vSpec As Variant, vOut As Variant
Dim I As Long, v As Variant
With ThisWorkbook
Set wsMain = .Worksheets("Main")
Set wsSpec = .Worksheets("Specimen")
Set wsOut = .Worksheets("Output")
End With
'Read data into vba arrays for processing speed
With wsMain
vMain = .Range(.Cells(1, 1), .Cells(.Rows.Count, 1).End(xlUp)).Resize(columnsize:=3)
End With
With wsSpec
vSpec = .Range(.Cells(1, 1), .Cells(.Rows.Count, 1).End(xlUp)).Resize(columnsize:=3)
End With
'add ID to names dictionary
Set dN = New Dictionary
For I = 2 To UBound(vMain, 1)
dN.Add Key:=vMain(I, 1), Item:=I
Next I
'add missing ID's to missing dictionary
Set dM = New Dictionary
For I = 2 To UBound(vSpec, 1)
If Not dN.Exists(vSpec(I, 1)) Then
dM.Add Key:=vSpec(I, 1), Item:=WorksheetFunction.Index(vSpec, I, 0)
End If
Next I
'write results to output array
ReDim vOut(0 To dM.Count, 1 To 3)
vOut(0, 1) = "UNIQUE ID"
vOut(0, 2) = "FIRST NAME"
vOut(0, 3) = "LAST NAME"
I = 0
For Each v In dM.Keys
I = I + 1
vOut(I, 1) = dM(v)(1)
vOut(I, 2) = dM(v)(2)
vOut(I, 3) = dM(v)(3)
Next v
Dim R As Range
With wsOut
Set R = .Cells(1, 1)
Set R = R.Resize(UBound(vOut, 1) + 1, UBound(vOut, 2))
With R
.EntireColumn.Clear
.Value = vOut
.Style = "Output"
.EntireColumn.AutoFit
End With
End With
End Sub
Both show the same result (except the formula solution does not bring over the column headers; but you can do that with a formula =mnTbl[#Headers] in the cell above the original formula above).
Another option is to join the values of each row in each range and store them in arrays.
Then compare arrays and output the unique values.
In this case, your uniques come from evaluating the whole row, and not just the Unique ID.
Please read code's comments and adjust it to fit your needs.
Public Sub OutputUniqueValues()
Dim mainSheet As Worksheet
Dim specimenSheet As Worksheet
Dim outputSheet As Worksheet
Dim mainRange As Range
Dim specimenRange As Range
Dim mainArray As Variant
Dim specimenArray As Variant
Dim mainFirstRow As Long
Dim specimenFirstRow As Long
Dim outputCounter As Long
Set mainSheet = ThisWorkbook.Worksheets("main")
Set specimenSheet = ThisWorkbook.Worksheets("specimen")
Set outputSheet = ThisWorkbook.Worksheets("output")
' Row at which the output range will be printed (not including headers)
outputCounter = 2
' Process main data ------------------------------------
' Row at which the range to be evaluated begins
mainFirstRow = 2
' Turn range rows into array items
mainArray = ProcessRangeData(mainSheet, mainFirstRow)
' Process specimen data ------------------------------------
' Row at which the range to be evaluated begins
specimenFirstRow = 2
' Turn range rows into array items
specimenArray = ProcessRangeData(specimenSheet, specimenFirstRow)
' Look for unique values and output results in sheet
OutputUniquesFromArrays outputSheet, outputCounter, mainArray, specimenArray
End Sub
Private Function ProcessRangeData(ByVal dataSheet As Worksheet, ByVal firstRow As Long) As Variant
Dim dataRange As Range
Dim evalRowRange As Range
Dim lastRow As Long
Dim counter As Long
Dim dataArray As Variant
' Get last row in sheet (column 1 = column A)
lastRow = dataSheet.Cells(dataSheet.Rows.Count, 1).End(xlUp).Row
' Set the range of specimen sheet
Set dataRange = dataSheet.Range("A" & firstRow & ":C" & lastRow)
' Redimension the array to the number of rows in range
ReDim dataArray(dataRange.Rows.Count)
counter = 0
' Join each row values so it's easier to compare them later and add them to an array
For Each evalRowRange In dataRange.Rows
' Use Trim function if you want to omit the first and last characters if they are spaces
dataArray(counter) = Trim(evalRowRange.Cells(1).Value) & "|" & Trim(evalRowRange.Cells(2).Value) & "|" & Trim(evalRowRange.Cells(3).Value)
counter = counter + 1
Next evalRowRange
ProcessRangeData = dataArray
End Function
Private Sub OutputUniquesFromArrays(ByVal outputSheet As Worksheet, ByVal outputCounter As Long, ByVal mainArray As Variant, ByVal specimenArray As Variant)
Dim specimenFound As Boolean
Dim specimenCounter As Long
Dim mainCounter As Long
' Look for unique values ------------------------------------
For specimenCounter = 0 To UBound(specimenArray)
specimenFound = False
' Check if value in specimen array exists in main array
For mainCounter = 0 To UBound(mainArray)
If specimenArray(specimenCounter) = mainArray(mainCounter) Then specimenFound = True
Next mainCounter
If specimenFound = False Then
' Write values to output sheet
outputSheet.Range("A" & outputCounter).Value = Split(specimenArray(specimenCounter), "|")(0)
outputSheet.Range("B" & outputCounter).Value = Split(specimenArray(specimenCounter), "|")(1)
outputSheet.Range("C" & outputCounter).Value = Split(specimenArray(specimenCounter), "|")(2)
outputCounter = outputCounter + 1
End If
Next specimenCounter
End Sub