Delete filtered data based on date range - excel

I'm looking to filter for blank cells in ColumnB which I've done below:
Dim f As Range
Set f = ActiveSheet.Rows(1)
f.AutoFilter _
Field:=2, _
Criteria1:="", _
VisibleDropdown:=True
From there I'd like to delete this data if the blank cells from the filter are between two specific dates.
The date range would be July 2019 to May 2020. So the condition would be 1 filter column b for blanks, then if column A and/or column c fall within the July 2019 - May 2020 date range then delete the filtered rows.
Thank you!

I don't know if there is a way to use built-in functions to get what you're looking for, but the following code will work. For a large number of rows this might take a while to run.
Public Sub cleanUp()
Dim sRow As Integer
Dim lRow As Integer
Dim rng As Excel.Range
sRow = 2 'set row where data starts
lRow = ThisWorkbook.Worksheets(1).Cells(1048576, 1).End(xlUp).Row 'find the last row of data
Set rng = ThisWorkbook.Worksheets(1).Cells(lRow, 2) 'start at the last row with data
For r = lRow To sRow Step -1 'work backwards to avoid skipping rows
With ThisWorkbook.Worksheets(1)
If .Cells(r, 2).Value = "" Then
'column B of row r was empty
If .Cells(r, 1).Value > 43647 And .Cells(r, 1).Value < 43982 Then
'column A of row r was between 1 July 2019 and 31 May 2020
'row should be deleted
.Rows(r).Delete
ElseIf .Cells(r, 3).Value > 43647 And .Cells(r, 1).Value < 43982 Then
'column C of row r was between 1 July 2019 and 31 May 2020
'row should be deleted
.Rows(r).Delete
Else
'dates in columns A and C did not match criteria
'do nothing
End If
Else
'column B of row r is not empty
'do nothing
End If
End With
Next r
End Sub

Within
Assumptions
The dates are dates, not strings.
Within July 2019 to May 2020 means 07/01/2019 00:00:00 <= n < 06/01/2020 00:00:00.
There is no data below the dataset.
Usage & Tips
Copy the complete code into a standard module, e.g. Module1.
Only run the first procedure (deleteWithin), the rest of them are being called by it, when necessary.
The constants are adjusted to your setup, but check them before running the code.
You should qualify the worksheet, probably the workbook, too. You would't want to run this code on the wrong worksheet.
Test the accuracy of the code first with Select (adjusted in deleteRows) and only afterwards change to Delete.
The Code
Option Explicit
Sub deleteWithin()
' Constants
Const FirstMMMMYYYY As String = "July 2019"
Const LastMMMMYYYY As String = "May 2020"
Const FirstRow As Long = 2 ' First Row of Data
Const Cols As String = "A:C" ' Address of All Columns
' The following 4 column numbers represent the n-th columns of 'Cols'.
Const LastRowColumn As Long = 1 ' Column to Calculate the Last Row
Const DateColumn1 As Long = 1 ' Column Containing the First Date.
Const DateColumn2 As Long = 3 ' Column Containing the Second Date.
Const CriteriaColumn As Long = 2 ' Column Containing the Criteria ("")
Const Criteria As String = ""
' Define Source Range ('rng').
Dim ws As Worksheet
Set ws = ActiveSheet
Dim rng As Range
Set rng = ws.Columns(LastRowColumn).Find("*", , xlFormulas, , , xlPrevious)
If rng Is Nothing Then
GoTo ProcExit
End If
If rng.Row < FirstRow Then
GoTo ProcExit
End If
Dim FirstColumn As Long
FirstColumn = ws.Columns(Cols).Column
Dim LastColumn As Long
LastColumn = FirstColumn + ws.Columns(Cols).Columns.Count - 1
Set rng = ws.Range(ws.Cells(FirstRow, FirstColumn), _
rng.Offset(, LastColumn - LastRowColumn))
' Write the numbers of the rows to delete to Data Rows Array ('DataRows').
' Write values from Source Range to Data Array ('Data').
Dim Data As Variant
Data = rng.Value
' Define Data Rows Array.
Dim DataRows As Variant
ReDim DataRows(1 To UBound(Data))
' Calculate Date Interval ('DateInterval').
Dim DateInterval As Variant
DateInterval = getDateInterval(FirstMMMMYYYY, LastMMMMYYYY)
' Declare additional variables to use in the following 'For Next' loop.
Dim FirstDate As Variant
Dim SecondDate As Variant
Dim i As Long
Dim k As Long
' Loop through rows of Data Array.
For i = 1 To UBound(Data)
' If Criteria is found then write row number to Data Rows Array.
If Data(i, CriteriaColumn) = Criteria Then
FirstDate = Data(i, DateColumn1)
SecondDate = Data(i, DateColumn2)
If fallsWithin(FirstDate, SecondDate, DateInterval) Then
k = k + 1
DataRows(k) = i + FirstRow - 1
Else
End If
End If
Next i
' Delete rows in worksheet.
' Check if at least once Criteria was found.
If k > 0 Then
' Resize Data Rows Array.
ReDim Preserve DataRows(1 To k)
' Delete rows in one go.
deleteRows ws, DataRows
MsgBox "Rows deleted."
Else
MsgBox "Nothing deleted."
End If
ProcExit:
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Converts two strings in the format "MMMM YYYY" to dates and
' returns a 1D two-element array whose first element is less than
' the second element. The first element is to be tested with ">=", while
' the second element is to be tested with "<".
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function getDateInterval(ByVal FirstMMMMYYYY As String, _
ByVal SecondMMMMYYYY As String) _
As Variant
Dim MMMM As Variant
MMMM = Array("January", "February", "March", "April", "May", "June", _
"July", "August", "September", "October", "November", _
"December")
Dim First As Date
First = DateSerial(CLng(Split(FirstMMMMYYYY)(1)), _
Application.Match(Split(FirstMMMMYYYY)(0), MMMM, 0), _
1)
Dim Second As Date
Second = DateSerial(CLng(Split(SecondMMMMYYYY)(1)), _
Application.Match(Split(SecondMMMMYYYY)(0), MMMM, 0) _
+ 1, _
1)
If First < Second Then
getDateInterval = Array(First, Second)
Else
getDateInterval = Array(Second, First)
End If
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Returns 'True' if two values are within the values of an interval.
' The interval is a 1D two-element array and its first element
' is less than its second element. The first element is to be tested with ">=",
' while the second element is to be tested with "<".
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function fallsWithin(ByVal FirstValue As Variant, _
ByVal SecondValue As Variant, _
Interval As Variant) _
As Boolean
Dim StartValue As Variant
StartValue = Interval(LBound(Interval))
Dim EndValue As Variant
EndValue = Interval(UBound(Interval))
If FirstValue < SecondValue Then
If FirstValue >= StartValue And SecondValue < EndValue Then
fallsWithin = True
End If
Else
If SecondValue >= StartValue And FirstValue < EndValue Then
fallsWithin = True
End If
End If
ProcExit:
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Assumes that 'Sheet' is a valid worksheet and 'Data' is a 1D array
' containing at least one row number.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Sub deleteRows(Sheet As Worksheet, _
DataRows As Variant)
Dim rng As Range
Set rng = Sheet.Rows(DataRows(LBound(DataRows)))
If UBound(DataRows) - LBound(DataRows) > 0 Then
Dim j As Long
For j = LBound(DataRows) + 1 To UBound(DataRows)
' 'Collect' row ranges into one range.
Set rng = Union(rng, Sheet.Rows(DataRows(j)))
Next j
End If
' Delete rows in one go.
rng.Select ' Change to 'rng.Delete' when tested.
End Sub

Related

extract unique data

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

Find Row Value in VBA Range Compare

I am comparing 2 rows, and I stuck on trying to copy the row value of the current cell in the range.
Problem:
I have a list of Employee Codes, I am comparing these employee codes and copying any data that does not exist (ie new employees). I also need to skip the "*" in my range compare
However I have a sheet where the data I need to copy is in the row below the row where the employee code is
Sheets are set up like this
WSUPER
A B C
XXXX ... ...
CCCC ... ...
VVVV ... ...
Marco_data
A B C D V W
xx xx xx XXXX
xx xx xx * ... ...
xx xx xx FFFF
xx xx xx * ... ...
What I need to copy is the different employee code (in this case FFFF (only this cell) and the Values in Columns V and W
This is the code I have, but does not work correctly
' insert new data - This checks each employees code from the raw and compares in the exisiting. If it doesn't exist it inserts the record. If it does it skips to the next
For Each rng In Sheets("Macro_data").Range(Cells(2, 4), Cells(Cells(Rows.Count, "D").End(xlUp).Row, 1))
Set wb1rng = Sheets("WSUPER").Range("A:A").Find(rng, LookIn:=xlValues, LookAt:=xlWhole)
' If rng Like "[A-Za-z0-9]" Then
If wb1rng Is Nothing Then
coprng = rng.Cells.Row
MsgBox coprng
rng.Cells(coprng, 4).Copy
Sheets("WSUPER").Cells(wbpaste, 1).PasteSpecial xlPasteValues
coprng = coprng + 1
Sheets("Macro_Data").Range(Cells(coprng, 21), Cells(coprng, 22)).Copy
Sheets("WSUPER").Cells(wbpaste, 2).PasteSpecial xlPasteValues
wbpaste = wbpaste + 1
addedrecords = addedrecords + 1
End If
' End If
Next rng
wbpaste is the last row + 1 which is will be the row to paste data in
the idea is to grab only employee code which does not exist ("D") and Column V and W from the row below.
I set the coprng as the range value, but this fails
To skip the * i tried the following, but all failed
If rng Like "[A-Za-z0-9]" Then
Any help would be great
Look Up New Records (feat. a 3D Array)
Adjust the constants as you see fit.
Option Explicit
Sub lookupNewRecords()
' Constants
Const srcName As String = "Macro_data"
Const srcFirstRow As Long = 2
Dim srcColumns As Variant: srcColumns = Array(4, 22, 23)
Const tgtName As String = "WSUPER"
Const tgtFirstRow As Long = 2
Const tgtColumn As Long = 1
' Other Variables
Dim src As Worksheet ' Source Worksheet (Object)
Dim tgt As Worksheet ' Target Worksheet (Object)
Dim rng As Range ' Source Criteria Column Last Non-Empty Cell,
' Source Criteria Column Range,
' Target Criteria Column Last Non-Empty Cell,
' Target Criteria Column Range
Dim Source(2) As Variant ' Source Array of Arrays (Columns)
Dim Target As Variant ' Target (Criteria Column) Array
Dim Result() As Variant ' Result Array
Dim tgtFR As Long ' Target First Row
Dim i As Long ' Source Arrays Row Counter
Dim j As Long ' Result Array Columns Counter
Dim k As Long ' Result Array Rows Counter,
' Source Array of Arrays Elements (Arrays) Counter
' Read Source Columns and write to Source Array of Arrays.
Set src = ThisWorkbook.Worksheets(srcName)
Set rng = src.Columns(srcColumns(0)) _
.Find("*", , xlFormulas, , , xlPrevious)
If rng Is Nothing Then Exit Sub
If rng.Row < srcFirstRow Then Exit Sub
Set rng = src.Range(src.Cells(srcFirstRow, srcColumns(0)), rng)
Source(0) = rng
' Note: If you don't need a row offset like OP did,
' delete just the "1"-s included in the "1, "-s.
Source(1) = rng.Offset(1, srcColumns(1) - srcColumns(0))
Source(2) = rng.Offset(1, srcColumns(2) - srcColumns(0))
' Read Target Criteria Column and write to Target Array.
Set tgt = ThisWorkbook.Worksheets(tgtName)
Set rng = tgt.Columns(tgtColumn) _
.Find("*", , xlFormulas, , , xlPrevious)
If rng Is Nothing Then Exit Sub
If rng.Row < tgtFirstRow Then Exit Sub
tgtFR = rng.Row + 1
Set rng = tgt.Range(tgt.Cells(tgtFirstRow, tgtColumn), rng)
Target = rng
Set rng = Nothing ' Necessary data is in Arrays.
' Write to Result Array.
For i = 1 To UBound(Source(0))
If Source(0)(i, 1) <> "*" And _
IsError(Application.Match(Source(0)(i, 1), Target, 0)) Then
ReDim Preserve Result(2, j)
For k = 0 To 2
Result(k, j) = Source(k)(i, 1)
Next k
j = j + 1
End If
Next i
If j = 0 Then GoTo NoData
' Write to Result Range.
tgt.Cells(tgtFR, tgtColumn) _
.Resize(UBound(Result, 2) + 1, UBound(Result) + 1) = _
Application.Transpose(Result)
' Note: The Result Array was transposed because you can only resize
' an array by its last dimension. In this case it had "3" rows and
' "j" columns, but we needed "j" rows and "3" columns.
' Inform user.
Dim msg As String
If j > 1 Then msg = "s"
MsgBox "Transferred " & j & " new record" & msg & ".", _
vbInformation, "Success"
Exit Sub
NoData:
MsgBox "No new records found.", vbExclamation, "No Transfer"
End Sub

Check if all column values exists in another list

The excel vba macro I have created goes through an entire column and searches each value in the column against another column found on another worksheet. I have a T/F column where I mark down "T" if it is found, "F" if it is not found. However, I feel like the way I am doing it might not be very efficient, as the macro takes about 30 minutes to go through 31,000 rows of values to be searched from another column with about 27,000 number of values.
For a simple illustration, I have included a few images which explains what the macro does.
Initially the T/F column will be empty. Only after executing the macro, would it be filled. I loop through each row in column A and try to find the value against the SearchCol in the next picture.
Here is the vba code I am currently using.
Sub CheckIfValuesExist()
Dim ActiveWS As Worksheet, WS2 As Worksheet
Dim ValueColLetter As String, SearchColLetter As String, TFColLetter As String
Dim LastRow As Long, i As Long
Dim target As Variant, rng As Range
Set ActiveWS = ActiveWorkbook.Worksheets(1)
Set WS2 = ActiveWorkbook.Worksheets(2)
ValueColLetter = "A"
SearchColLetter = "A"
TFColLetter = "B"
LastRow = ActiveWS.Cells.Find(What:="*", SearchOrder:=xlRows, _
SearchDirection:=xlPrevious, _
LookIn:=xlFormulas).Row
For i = 2 To LastRow
target = ActiveWS.Range(ValueColLetter & i).Value
If target <> "" Then
With WS2.Range(SearchColLetter & ":" & SearchColLetter) 'searches all of column A
Set rng = .Find(What:=target, _
After:=.Cells(.Cells.Count), _
LookIn:=xlValues, _
LookAt:=xlWhole, _
SearchOrder:=xlByRows, _
SearchDirection:=xlNext, _
MatchCase:=False)
If Not rng Is Nothing Then
ActiveWS.Range(TFColLetter & i).Value = "T" 'value found
Else
ActiveWS.Range(TFColLetter & i).Value = "F" 'value not found
End If
End With
End If
Next i
End Sub
The macro works as intended, I just find it to be slow. Is there any better way to do the same thing but in a quicker manner?
Check Column Against Column
Array Match Range Version
Sub CheckIfValuesExist()
Const cSheet1 As Variant = 1 ' Value Worksheet Name/Index
Const cSheet2 As Variant = 2 ' Search Worksheet Name/Index
Const cFirst As Long = 2 ' First Row
Const cVal As Variant = "A" ' Value Column
Const cSrc As Variant = "A" ' Search Column
Const cTF As Variant = "B" ' Target Column
Const cT As String = "T" ' Found String
Const cF As String = "F" ' Not Found String
Dim RngS As Range ' Search Range
Dim vntV As Variant ' Value Array
Dim vntT As Variant ' Target Array
Dim LastV As Long ' Value Last Column Number
Dim LastS As Long ' Search Last Column Number
Dim i As Long ' Value/Target Row Counter
Dim dummy As Long ' Match Dummy Variable
With Application
.Calculation = xlCalculationManual
.ScreenUpdating = False
End With
On Error GoTo ProcedureExit
With ThisWorkbook.Worksheets(cSheet1)
LastV = .Columns(.Cells(1, cVal).Column).Find("*", , -4123, , 2, 2).Row
vntV = .Range(.Cells(cFirst, cVal), .Cells(LastV, cVal))
End With
With ThisWorkbook.Worksheets(cSheet2)
LastS = .Columns(.Cells(1, cSrc).Column).Find("*", , -4123, , 2, 2).Row
Set RngS = .Range(.Cells(cFirst, cSrc), .Cells(LastS, cSrc))
ReDim vntT(1 To UBound(vntV), 1 To 1)
For i = 1 To UBound(vntV)
On Error Resume Next
If vntV(i, 1) <> "" Then
dummy = Application.Match(vntV(i, 1), RngS, 0)
If Err Then
vntT(i, 1) = cF
Else
vntT(i, 1) = cT
End If
End If
On Error GoTo 0
Next
End With
On Error GoTo ProcedureExit
With ThisWorkbook.Worksheets(cSheet1)
.Range(.Cells(cFirst, cTF), .Cells(.Rows.Count, cTF)).ClearContents
.Cells(cFirst, cTF).Resize(UBound(vntT)) = vntT
End With
ProcedureExit:
With Application
.Calculation = xlCalculationAutomatic
.ScreenUpdating = True
End With
End Sub
Let us assume that data included in Sheet 1.
Try:
Option Explicit
Sub VlookUp()
Dim LastRowSV As Long, LastRowV As Long, Counts As Long
Dim wsName As String
Dim wsListSV As Range, cellSV As Range, wsListV As Range, cellV As Range
With ThisWorkbook.Worksheets("Sheet1")
'Find the last row of Search Values
LastRowSV = .Cells(.Rows.Count, "D").End(xlUp).Row
'Find the last row of Values
LastRowV = .Cells(.Rows.Count, "A").End(xlUp).Row
'Set the list with the Search Values
Set wsListSV = .Range(Cells(2, 4), Cells(LastRowSV, 4))
'Set the list with the Values
Set wsListV = .Range(Cells(3, 1), Cells(LastRowV, 1))
'Loop each value in Search Values
For Each cellV In wsListV
Counts = Application.WorksheetFunction.CountIf(wsListSV, cellV)
If Counts <> 0 Then
cellV.Offset(0, 1).Value = "T"
Else
cellV.Offset(0, 1).Value = "F"
End If
Next
End With
End Sub
Result:
Why don't you use the MATCH formula?
If your values are in Col A and the search values are at
the cells $F$5:$F$10 the formula is:
=MATCH(A2,$F$5:$F$10,0)
or if you insist on a T/F result:
=IF(ISERROR(MATCH(A2,$F$5:$F$10,0)),"T","F")
Of cause you can insert this formula also with a macro.

Split cell values into multiple rows and keep other data

I have values in column B separated by commas. I need to split them into new rows and keep the other data the same.
I have a variable number of rows.
I don't know how many values will be in the cells in Column B, so I need to loop over the array dynamically.
Example:
ColA ColB ColC ColD
Monday A,B,C Red Email
Output:
ColA ColB ColC ColD
Monday A Red Email
Monday B Red Email
Monday C Red Email
Have tried something like:
colArray = Split(ws.Cells(i, 2).Value, ", ")
For i = LBound(colArray) To UBound(colArray)
Rows.Insert(i)
Next i
Try this, you can easily adjust it to your actual sheet name and column to split.
Sub splitByColB()
Dim r As Range, i As Long, ar
Set r = Worksheets("Sheet1").Range("B999999").End(xlUp)
Do While r.row > 1
ar = Split(r.value, ",")
If UBound(ar) >= 0 Then r.value = ar(0)
For i = UBound(ar) To 1 Step -1
r.EntireRow.Copy
r.Offset(1).EntireRow.Insert
r.Offset(1).value = ar(i)
Next
Set r = r.Offset(-1)
Loop
End Sub
You can also just do it in place by using a Do loop instead of a For loop. The only real trick is to just manually update your row counter every time you insert a new row. The "static" columns that get copied are just a simple matter of caching the values and then writing them to the inserted rows:
Dim workingRow As Long
workingRow = 2
With ActiveSheet
Do While Not IsEmpty(.Cells(workingRow, 2).Value)
Dim values() As String
values = Split(.Cells(workingRow, 2).Value, ",")
If UBound(values) > 0 Then
Dim colA As Variant, colC As Variant, colD As Variant
colA = .Cells(workingRow, 1).Value
colC = .Cells(workingRow, 3).Value
colD = .Cells(workingRow, 4).Value
For i = LBound(values) To UBound(values)
If i > 0 Then
.Rows(workingRow).Insert xlDown
End If
.Cells(workingRow, 1).Value = colA
.Cells(workingRow, 2).Value = values(i)
.Cells(workingRow, 3).Value = colC
.Cells(workingRow, 4).Value = colD
workingRow = workingRow + 1
Next
Else
workingRow = workingRow + 1
End If
Loop
End With
This will do what you want.
Option Explicit
Const ANALYSIS_ROW As String = "B"
Const DATA_START_ROW As Long = 1
Sub ReplicateData()
Dim iRow As Long
Dim lastrow As Long
Dim ws As Worksheet
Dim iSplit() As String
Dim iIndex As Long
Dim iSize As Long
'Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
With ThisWorkbook
.Worksheets("Sheet4").Copy After:=.Worksheets("Sheet4")
Set ws = ActiveSheet
End With
With ws
lastrow = .Cells(.Rows.Count, ANALYSIS_ROW).End(xlUp).Row
End With
For iRow = lastrow To DATA_START_ROW Step -1
iSplit = Split(ws.Cells(iRow, ANALYSIS_ROW).Value2, ",")
iSize = UBound(iSplit) - LBound(iSplit) + 1
If iSize = 1 Then GoTo Continue
ws.Rows(iRow).Copy
ws.Rows(iRow).Resize(iSize - 1).Insert
For iIndex = LBound(iSplit) To UBound(iSplit)
ws.Cells(iRow, ANALYSIS_ROW).Offset(iIndex).Value2 = iSplit(iIndex)
Next iIndex
Continue:
Next iRow
Application.CutCopyMode = False
Application.Calculation = xlCalculationAutomatic
'Application.ScreenUpdating = True
End Sub
A formula solution is close to your requirement.
Cell G1 is the delimiter. In this case a comma.
Helper E1:=SUM(E1,LEN(B1)-LEN(SUBSTITUTE(B1,$H$1,"")))+1
You must fill the above formula one row more.
A8:=a1
Fill this formula to the right.
A9:=LOOKUP(ROW(1:1),$E:$E,A:A)&""
Fill this formula to the right and then down.
B9:=MID($H$1&LOOKUP(ROW(A1),E:E,B:B)&$H$1,FIND("艹",SUBSTITUTE($H$1&LOOKUP(ROW(A1),E:E,B:B)&$H$1,$H$1,"艹",ROW(A2)-LOOKUP(ROW(A1),E:E)))+1,FIND("艹",SUBSTITUTE($H$1&LOOKUP(ROW(A1),E:E,B:B)&$H$1,$H$1,"艹",ROW(A2)-LOOKUP(ROW(A1),E:E)+1))-FIND("艹",SUBSTITUTE($H$1&LOOKUP(ROW(A1),E:E,B:B)&$H$1,$H$1,"艹",ROW(A2)-LOOKUP(ROW(A1),E:E)))-1)&""
Fill down.
Bug:
Numbers will be converted to Text. Of course you can remove the &"" at the end of the formula, but blank cells will be filled with 0.
Given #A.S.H.'s excellent and brief answer, the VBA function below might be a bit of an overkill, but it will hopefully be of some help to someone looking for a more "generic" solution. This method makes sure not to modify the cells to the left, to the right, or above the table of data, in case the table does not start in A1 or in case there is other data on the sheet besides the table. It also avoids copying and inserting entire rows, and it allows you to specify a separator other than a comma.
This function happens to have similarities to #ryguy72's procedure, but it does not rely on the clipboard.
Function SplitRows(ByRef dataRng As Range, ByVal splitCol As Long, ByVal splitSep As String, _
Optional ByVal idCol As Long = 0) As Boolean
SplitRows = True
Dim oldUpd As Variant: oldUpd = Application.ScreenUpdating
Dim oldCal As Variant: oldCal = Application.Calculation
On Error GoTo err_sub
'Modify application settings for the sake of speed
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
'Get the current number of data rows
Dim rowCount As Long: rowCount = dataRng.Rows.Count
'If an ID column is specified, use it to determine where the table ends by finding the first row
' with no data in that column
If idCol > 0 Then
With dataRng
rowCount = .Offset(, idCol - 1).Resize(, 1).End(xlDown).Row - .Row + 1
End With
End If
Dim splitArr() As String
Dim splitLb As Long, splitUb As Long, splitI As Long
Dim editedRowRng As Range
'Loop through the data rows to split them as needed
Dim r As Long: r = 0
Do While r < rowCount
r = r + 1
'Split the string in the specified column
splitArr = Split(dataRng.Cells(r, splitCol).Value & "", splitSep)
splitLb = LBound(splitArr)
splitUb = UBound(splitArr)
'If the string was not split into more than 1 item, skip this row
If splitUb <= splitLb Then GoTo splitRows_Continue
'Replace the unsplit string with the first item from the split
Set editedRowRng = dataRng.Resize(1).Offset(r - 1)
editedRowRng.Cells(1, splitCol).Value = splitArr(splitLb)
'Create the new rows
For splitI = splitLb + 1 To splitUb
editedRowRng.Offset(1).Insert 'Add a new blank row
Set editedRowRng = editedRowRng.Offset(1) 'Move down to the next row
editedRowRng.Offset(-1).Copy Destination:=editedRowRng 'Copy the preceding row to the new row
editedRowRng.Cells(1, splitCol).Value = splitArr(splitI) 'Place the next item from the split string
'Account for the new row in the counters
r = r + 1
rowCount = rowCount + 1
Next
splitRows_Continue:
Loop
exit_sub:
On Error Resume Next
'Resize the original data range to reflect the new, full data range
If rowCount <> dataRng.Rows.Count Then Set dataRng = dataRng.Resize(rowCount)
'Restore the application settings
If Application.ScreenUpdating <> oldUpd Then Application.ScreenUpdating = oldUpd
If Application.Calculation <> oldCal Then Application.Calculation = oldCal
Exit Function
err_sub:
SplitRows = False
Resume exit_sub
End Function
Function input and output
To use the above function, you would specify
the range containing the rows of data (excluding the header)
the (relative) number of the column within the range with the string to split
the separator in the string to split
the optional (relative) number of the "ID" column within the range (if a number >=1 is provided, the first row with no data in this column will be taken as the last row of data)
The range object passed in the first argument will be modified by the function to reflect the range of all the new data rows (including all inserted rows). The function returns True if no errors were encountered, and False otherwise.
Examples
For the range illustrated in the original question, the call would look like this:
SplitRows Range("A2:C2"), 2, ","
If the same table started in F5 instead of A1, and if the data in column G (i.e. the data that would fall in column B if the table started in A1) was separated by Alt-Enters instead of commas, the call would look like this:
SplitRows Range("F6:H6"), 2, vbLf
If the table contained the row header plus 10 rows of data (instead of 1), and if it started in F5 again, the call would look like this:
SplitRows Range("F6:H15"), 2, vbLf
If there was no certainty about the number of rows, but we knew that all the valid rows are contiguous and always have a value in column H (i.e. the 3rd column in the range), the call could look something like this:
SplitRows Range("F6:H1048576"), 2, vbLf, 3
In Excel 95 or lower, you would have to change "1048576" to "16384", and in Excel 97-2003, to "65536".

Populate unique values into a VBA array from Excel

Can anyone give me VBA code that will take a range (row or column) from an Excel sheet and populate a list/array with the unique values,
i.e.:
table
table
chair
table
stool
stool
stool
chair
when the macro runs would create an array some thing like:
fur[0]=table
fur[1]=chair
fur[2]=stool
Sub GetUniqueAndCount()
Dim d As Object, c As Range, k, tmp As String
Set d = CreateObject("scripting.dictionary")
For Each c In Selection
tmp = Trim(c.Value)
If Len(tmp) > 0 Then d(tmp) = d(tmp) + 1
Next c
For Each k In d.keys
Debug.Print k, d(k)
Next k
End Sub
In this situation I always use code like this (just make sure delimeter you've chosen is not a part of search range)
Dim tmp As String
Dim arr() As String
If Not Selection Is Nothing Then
For Each cell In Selection
If (cell <> "") And (InStr(tmp, cell) = 0) Then
tmp = tmp & cell & "|"
End If
Next cell
End If
If Len(tmp) > 0 Then tmp = Left(tmp, Len(tmp) - 1)
arr = Split(tmp, "|")
Combining the Dictionary approach from Tim with the variant array from Jean_Francois below.
The array you want is in objDict.keys
Sub A_Unique_B()
Dim X
Dim objDict As Object
Dim lngRow As Long
Set objDict = CreateObject("Scripting.Dictionary")
X = Application.Transpose(Range([a1], Cells(Rows.Count, "A").End(xlUp)))
For lngRow = 1 To UBound(X, 1)
objDict(X(lngRow)) = 1
Next
Range("B1:B" & objDict.Count) = Application.Transpose(objDict.keys)
End Sub
This is the old-school way of doing it.
It will execute faster than looping through cells (e.g. For Each cell In Selection) and will be reliable no matter what, as long you have a rectangular selection (i.e. not Ctrl-selecting a bunch of random cells).
Sub FindUnique()
Dim varIn As Variant
Dim varUnique As Variant
Dim iInCol As Long
Dim iInRow As Long
Dim iUnique As Long
Dim nUnique As Long
Dim isUnique As Boolean
varIn = Selection
ReDim varUnique(1 To UBound(varIn, 1) * UBound(varIn, 2))
nUnique = 0
For iInRow = LBound(varIn, 1) To UBound(varIn, 1)
For iInCol = LBound(varIn, 2) To UBound(varIn, 2)
isUnique = True
For iUnique = 1 To nUnique
If varIn(iInRow, iInCol) = varUnique(iUnique) Then
isUnique = False
Exit For
End If
Next iUnique
If isUnique = True Then
nUnique = nUnique + 1
varUnique(nUnique) = varIn(iInRow, iInCol)
End If
Next iInCol
Next iInRow
'// varUnique now contains only the unique values.
'// Trim off the empty elements:
ReDim Preserve varUnique(1 To nUnique)
End Sub
Profiting from the MS Excel 365 function UNIQUE()
In order to enrich the valid solutions above:
Sub ExampleCall()
Dim rng As Range: Set rng = Sheet1.Range("A2:A11") ' << change to your sheet's Code(Name)
Dim a: a = rng
a = getUniques(a)
arrInfo a
End Sub
Function getUniques(a, Optional ZeroBased As Boolean = True)
Dim tmp: tmp = Application.Transpose(WorksheetFunction.Unique(a))
If ZeroBased Then ReDim Preserve tmp(0 To UBound(tmp) - 1)
getUniques = tmp
End Function
OK I did it finally:
Sub CountUniqueRecords()
Dim Array() as variant, UniqueArray() as variant, UniqueNo as Integer,
Dim i as integer, j as integer, k as integer
Redim UnquiArray(1)
k= Upbound(array)
For i = 1 To k
For j = 1 To UniqueNo + 1
If Array(i) = UniqueArray(j) Then GoTo Nx
Next j
UniqueNo = UniqueNo + 1
ReDim Preserve UniqueArray(UniqueNo + 1)
UniqueArray(UniqueNo) = Array(i)
Nx:
Next i
MsgBox UniqueNo
End Sub
one more way ...
Sub get_unique()
Dim unique_string As String
lr = Sheets("data").Cells(Sheets("data").Rows.Count, 1).End(xlUp).Row
Set range1 = Sheets("data").Range("A2:A" & lr)
For Each cel In range1
If Not InStr(output, cel.Value) > 0 Then
unique_string = unique_string & cel.Value & ","
End If
Next
End Sub
This VBA function returns an array of distinct values when passed either a range or a 2D array source
It defaults to processing the first column of the source, but you can optionally choose another column.
I wrote a LinkedIn article about it.
Function DistinctVals(a, Optional col = 1)
Dim i&, v: v = a
With CreateObject("Scripting.Dictionary")
For i = 1 To UBound(v): .Item(v(i, col)) = 1: Next
DistinctVals = Application.Transpose(.Keys)
End With
End Function
The old school method was my favourite option. Thank you. And it was indeed fast. But I didn't use redim. Here though is my real world example where I accumulate values for each unique "key" found in a column and move it into a array (say for an employee and values are hours worked per day). Then I put each key with its final values into a totals area on the active sheet. I've commented extensively for anyone who wants painful detail on what is happening here. Limited error checking is done by this code.
Sub GetActualTotals()
'
' GetActualTotals Macro
'
' This macro accumulates values for each unique employee from the active
' spreadsheet.
'
' History
' October 2016 - Version 1
'
' Invocation
' I created a button labeled "Get Totals" on the Active Sheet that invokes
' this macro.
'
Dim ResourceName As String
Dim TotalHours As Double
Dim TotalPercent As Double
Dim IsUnique As Boolean
Dim FirstRow, LastRow, LastColumn, LastResource, nUnique As Long
Dim CurResource, CurrentRow, i, j As Integer
Dim Resource(1000, 2) As Variant
Dim Rng, r As Range
'
' INITIALIZATIONS
'
' These are index numbers for the Resource array
'
Const RName = 0
Const TotHours = 1
Const TotPercent = 2
'
' Set the maximum number of resources we'll
' process.
'
Const ResourceLimit = 1000
'
' We are counting on there being no unintended data
' in the spreadsheet.
'
' It won't matter if the cells are empty though. It just
' may take longer to run the macro.
' But if there is data where this macro does not expect it,
' assume unpredictable results.
'
' There are some hardcoded values used.
' This macro just happens to expect the names to be in Column C (or 3).
'
' Get the last row in the spreadsheet:
'
LastRow = Cells.Find(What:="*", _
After:=Range("C1"), _
LookAt:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Row
'
' Furthermore, this macro banks on the first actual name to be in C6.
' so if the last row is row 65, the range we'll work with
' will evaluate to "C6:C65"
'
FirstRow = 6
Rng = "C" & FirstRow & ":C" & LastRow
Set r = Range(Rng)
'
' Initialize the resource array to be empty (even though we don't really
' need to but I'm old school).
'
For CurResource = 0 To ResourceLimit
Resource(CurResource, RName) = ""
Resource(CurResource, TotHours) = 0
Resource(CurResource, TotPercent) = 0
Next CurResource
'
' Start the resource counter at 0. The counter will represent the number of
' unique entries.
'
nUnique = 0
'
' LET'S GO
'
' Loop from the first relative row and the last relative row
' to process all the cells in the spreadsheet we are interested in
'
For i = 1 To LastRow - FirstRow
'
' Loop here for all unique entries. For any
' new unique entry, that array element will be
' initialized in the second if statement.
'
IsUnique = True
For j = 1 To nUnique
'
' If the current row element has a resource name and is already
' in the resource array, then accumulate the totals for that
' Resource Name. We then have to set IsUnique to false and
' exit the for loop to make sure we don't populate
' a new array element in the next if statement.
'
If r.Cells(i, 1).Value = Resource(j, RName) Then
IsUnique = False
Resource(j, TotHours) = Resource(j, TotHours) + _
r.Cells(i, 4).Value
Resource(j, TotPercent) = Resource(j, TotPercent) + _
r.Cells(i,5).Value
Exit For
End If
Next j
'
' If the resource name is unique then copy the initial
' values we find into the next resource array element.
' I ignore any null cells. (If the cell has a blank you might
' want to add a Trim to the cell). Not much error checking for
' the numerical values either.
'
If ((IsUnique) And (r.Cells(i, 1).Value <> "")) Then
nUnique = nUnique + 1
Resource(nUnique, RName) = r.Cells(i, 1).Value
Resource(nUnique, TotHours) = Resource(nUnique, TotHours) + _
r.Cells(i, 4).Value
Resource(nUnique, TotPercent) = Resource(nUnique, TotPercent) + _
r.Cells(i, 5).Value
End If
Next i
'
' Done processing all rows
'
' (For readability) Set the last resource counter to the last value of
' nUnique.
' Set the current row to the first relative row in the range (r=the range).
'
LastResource = nUnique
CurrentRow = 1
'
' Populate the destination cells with the accumulated values for
' each unique resource name.
'
For CurResource = 1 To LastResource
r.Cells(CurrentRow, 7).Value = Resource(CurResource, RName)
r.Cells(CurrentRow, 8).Value = Resource(CurResource, TotHours)
r.Cells(CurrentRow, 9).Value = Resource(CurResource, TotPercent)
CurrentRow = CurrentRow + 1
Next CurResource
End Sub
The VBA script below looks for all unique values from cell B5 all the way down to the very last cell in column B… $B$1048576. Once it is found, they are stored in the array (objDict).
Private Const SHT_MASTER = “MASTER”
Private Const SHT_INST_INDEX = “InstrumentIndex”
Sub UniqueList()
Dim Xyber
Dim objDict As Object
Dim lngRow As Long
Sheets(SHT_MASTER).Activate
Xyber = Application.Transpose(Sheets(SHT_MASTER).Range([b5], Cells(Rows.count, “B”).End(xlUp)))
Sheets(SHT_INST_INDEX).Activate
Set objDict = CreateObject(“Scripting.Dictionary”)
For lngRow = 1 To UBound(Xyber, 1)
If Len(Xyber(lngRow)) > 0 Then objDict(Xyber(lngRow)) = 1
Next
Sheets(SHT_INST_INDEX).Range(“B1:B” & objDict.count) = Application.Transpose(objDict.keys)
End Sub
I have tested and documented with some screenshots of the this solution. Here is the link where you can find it....
http://xybernetics.com/techtalk/excelvba-getarrayofuniquevaluesfromspecificcolumn/
If you don't mind using the Variant data type, then you can use the in-built worksheet function Unique as shown.
sub unique_results_to_array()
dim rng_data as Range
set rng_data = activesheet.range("A1:A10") 'enter the range of data here
dim my_arr() as Variant
my_arr = WorksheetFunction.Unique(rng_data)
first_val = my_arr(1,1)
second_val = my_arr(2,1)
third_val = my_arr(3,1) 'etc...
end sub
If you are not interested in the count function, then you could simplify the dictionary approach by using empty quotes for the dictionary value instead of the counter. The following code assumes the first cell containing data is "A1". Alternatively, you could use the Selection (though I understand that is generally frowned upon) or the sheet's UsedRange attribute as your range.
Both of the following examples assume that you want to omit blank values from your array of unique values.
Note that to utilize dictionary objects as follows, you must have the Microsoft Scripting Runtime library active in your references. Also note that by declaring dict as a New Dictionary instead of a Dictionary in the beginning, you can forgo the step of setting it equal to a Scripting Dictionary later. Also, dictionary keys must be unique, and this method does not result in errors when setting the value corresponding to a given dictionary key, so there is no risk of having unique keys.
Sub GetUniqueValuesInRange()
Dim cll As Range
Dim rng As Range
Dim dict As New Dictionary
Dim vArray As Variant
Set rng = Range("A1").CurrentRegion.Columns(1)
For Each cll In rng.Cells
If Len(cll.Value) > 0 Then
dict(cll.Value) = ""
End If
Next cll
vArray = dict.Keys
End Sub
The prior example is a slower method, as it is generally preferred to move the values into an array in the beginning, so that all calculations can be performed in the memory. The following should work faster for larger data sets:
Sub GetUniqueValuesInRange2()
Dim vFullArray As Variant
Dim var As Variant
Dim dict As New Dictionary
Dim vUniqueArray As Variant
vFullArray = Range("A1").CurrentRegion.Columns(1).Value
For Each var In vFullArray
If Len(var) > 0 Then
dict(var) = ""
End If
Next var
vUniqueArray = dict.Keys
End Sub

Resources