Getting unique values from a column - excel

The task at hand is to search in column A to see what values I have (they are in form of letters) and paste for each unique entry, its value once in another column.
Here is a visual explanation:
What I came up with was to create a For loop that iritiates through column A and created a conditional that if it found a certain value then it would insert the value in the range. Here is the code:
For i = 1 to 26
if cells(i,26).value= "A" Then
Range ("C1")= "A"
Elseif cells(i,26).value = "B" then
Range ("C2").value = "B"
ElseIf (i,26).value = "C" then
Range ("C3").value = "C"
EndIf
Next i
end sub
I want to cut this process short as my data set is really big with lots of company names. Any recommendations? I believe there has to be a way of knowing the values without having to look at all the values yourself.

If the goal is to just get a unique list of values found in Column A output to Column C you can use the below macro. This is really just recreating the steps of one method you would manually take to find unique values. Not the most sophisticated solution, but it works
Create a copy of your column with company names (using last available column in sheet)
De-dup the helper column
Copy the de-duped column to destination
Delete the helper column
Assumes the last column on worksheet is not used
Sub Unique()
Dim ws As Worksheet: Set ws = ThisWorkbook.Sheets("Sheet1")
Dim lr As Long, lc As Long
'Determine Range Size
lr = ws.Range("A" & ws.Rows.Count).End(xlUp).Row
lc = ws.Cells(1, ws.Columns.Count).Column
'Copy Company Names To Helper Column/Remove Duplicates
ws.Range("A2:A" & lr).Copy ws.Cells(1, lc)
ws.Columns(lc).RemoveDuplicates Columns:=1, Header:=xlNo
lr = ws.Cells(ws.Rows.Count, lc).End(xlUp).Row
'Output Unique Values From Helper Column
ws.Range(ws.Cells(1, lc), ws.Cells(lr, lc)).Copy
ws.Range("C2").PasteSpecial xlPasteValues
'Delete Helper Column
ws.Columns(lc).Delete
End Sub
Note my comment on post. VBA may not be needed here at all

Here's a slightly different version of using .RemoveDuplicates which also removes blank cells.
You can also do this without VBA. Just copy the desired column to another and use Remove Duplicates under Data tab.
Sub Unique_Values()
Dim ws As Worksheet: Set ws = ThisWorkbook.Sheets("Sheet1")
'Getting all the values in column A (except header)
'Copying them into cell C2 and below
ws.Range("A2", Range("A1048576").End(xlUp)).Copy Range("C2")
'setting the header for the column C
ws.Range("C1").Value = "What companies are in Column A?"
'Removing duplicates and blanks from column C
With ws.Range("$C$2", Range("C1048576").End(xlUp))
.Value = .Value
.RemoveDuplicates Columns:=1, Header:=xlNo
On Error Resume Next
.SpecialCells(xlCellTypeBlanks).Delete xlShiftUp
On Error GoTo 0
End With
End Sub
Although I agree with the coding convention used in the other answer, I think it is over-complicating the problem a little bit that would cause confusion for beginners.

I think both answers so far will give you exactly what you want, and perhaps could be simplified even further?
Sub GetUniqueQuick()
Dim LastRow As Long
Application.ScreenUpdating = False
LastRow = Sheets("Sheet1").Cells(Rows.Count, 1).End(xlUp).Row
Sheets("Sheet1").Range("A2:A" & LastRow).Copy Sheets("Sheet1").Range("C2")
Sheets("Sheet1").Range("C1:C" & LastRow).RemoveDuplicates Columns:=1, Header:=xlYes
End Sub

Using the dynamic features of MS 365 you can simply apply the worksheet function UNIQUE() over a given range, e.g.
= UNIQUE(A2:A100)
or integrate it in a user defined function
Function GetCompanies(rng As Range)
If rng.Columns.Count > 1 Then Exit Function ' allow only one column
GetCompanies = Application.Unique(rng) ' return function result as 2-dim array
End Function
As empty cells result in pseudo-uniques with a 0 output, you could call them in formula with an added cosmetical blank string :
=GetCompanies(A2:A100)&""

Related

Excel VBA: End(xlUp) and End(xlDown) all end up at row 244, which is blank?

I filled B2:GQ244 with formulae, copied the range and pasted by value before sorting the range column by column. The cells in B8:GQ244 were all blanks. Then, I wanted to concatenate the non-blank cells column by column, starting from row 2. To do so, I needed to find the last non-blank cell in each column.
For some reason, both End(xlUp) and End(xlDown) gave row 244, which was empty. I can't figure out why. I thought the file might be corrupted. So, I copied the two sheets and the module to a newly created workbook to no avail. Any explanation why both End(xlUp) and End(xlDown) gave row 244?
.Range("B2:GQ244").Formula = "=IF(ISERROR(FIND( B$1,Sheet9!$H34)),"""",Sheet9!$I34)"
'paste by value to get rid of formulae
.Range("B2:GQ244").Copy
.Range("B2").PasteSpecial Paste:=xlPasteValues
'sort by column
Dim last_row As Long
Dim j As Long
For i = 2 To 200 Step 1
Range(.Cells(2, i), .Cells(245, i)).Sort key1:=.Cells(2, i), order1:=xlAscending
Next i
For i = 2 To 200 Step 1
last_row = .Cells(65536, i).End(xlUp).Row
last_row = .Cells(1, i).End(xlDown).Row
The code below will remove all null strings at the bottom of columns as well as those that contain zeroes.
Sub ClearBlankCells()
' 146
Dim Rng As Range ' working range
Dim R As Long ' intermediate: row
Dim C As Long ' loop counter: columns
Application.ScreenUpdating = False
With ActiveSheet
With .Range("B2:GQ244")
.Formula = "=IF(ISERROR(FIND( B$1,Sheet9!$H34)),"""",Sheet9!$I34)"
' replace formulas with their values
.Copy
.PasteSpecial Paste:=xlPasteValues
End With
Application.CutCopyMode = False
For C = 2 To 200 Step 1
Set Rng = .Columns(C)
R = Application.Evaluate("SUMPRODUCT((" & Rng.Address & "<>"""")*(" & _
Rng.Address & "<>0)*1)")
If R > 0 Then
Set Rng = Range(.Cells(R + 1, C), .Cells(Rows.Count, C))
Rng.ClearContents
End If
' sort by column
' Range(.Cells(2, C), .Cells(245, C)).Sort Key1:=.Cells(2, C), Order1:=xlAscending
Next C
End With
Application.ScreenUpdating = True
End Sub
Note that no blanks or zeroes may be included in the block of data above the bottom of each column, including the caption.
Sorting must be done after such cells have been removed but I left the sort instructions dimmed out because it's wrong either in syntax or by concept. If you need to sort each column the syntax is wrong because the syntax sorts the entire sheet. On the other hand, if you want to sort the entire sheet you don't have to do it in a loop 200 times.
The code runs very slowly which gives rise to two observations.
It spends 99% of its time repairing the damage it has done in its first line.
It looks at a data range which is vastly bigger than what is actually, reasonably, required. Nobody wants to look at a sheet 200 columns and 244 rows.
Therefore there must be much better ways to do achieve what you want.
I can't confirm your findings. Having a blank ActiveSheet and a blank Sheet9 the code below filled the ActiveSheet with zeroes B2:GQ244. It then read the last row xlUp as 244 and xlDown as 2. Both of these values are as expected. Perhaps you have a setting that suppresses the display of zeroes. However, as explained in my comment above, a cell that appears blank isn't necessarily blank and that would also apply to a cell containing a NullString inserted by your formula, even if the formula was subsequently removed leaving the null string in its place.
Sub Examine()
Dim last_row As Long
Dim i As Long
With ActiveSheet
.Range("B2:GQ244").Formula = "=IF(ISERROR(FIND( B$1,Sheet9!$H34)),"""",Sheet9!$I34)"
'paste by value to get rid of formulae
.Range("B2:GQ244").Copy
.Range("B2").PasteSpecial Paste:=xlPasteValues
'sort by column
For i = 2 To 200 Step 1
Range(.Cells(2, i), .Cells(245, i)).Sort Key1:=.Cells(2, i), Order1:=xlAscending
last_row = .Cells(.Rows.Count, i).End(xlUp).Row
Debug.Print last_row ' returns 244
last_row = .Cells(1, i).End(xlDown).Row
Debug.Print last_row ' returns 2
Next i
End With
End Sub
The only mystery remaining, therefore, is why .Cells(1, i).End(xlDown).Row gives you a value of 244. It doesn't. Therefore the solution must be in the conduct of your test, not in its result. Compare your testing method with the one I employed above.

Last Row Returns 1 - incorrect value

situation is following:
I have 32 columns with data (various number of rows in columns) and need to delete cells with .value "downloaded" (always last cell in a column).
I have a code looping from column 32 to 1 and searching last_row for "downloaded" value. For 30 columns code seems to be working flawlessly but 2 columns return last_row value 1 even though there are multiple values (in fact hundreds of them) but they are non existent for VBA code.
Code:
Last_Col = ws.Cells(1, ws.Columns.Count).End(xlToLeft).Column
last_row = ws.Cells(Rows.Count & Last_Col).End(xlUp).Row
For R = Last_Col To 1 Step -1
With ws
Last_Col = R
last_row = ws.Cells(.Rows.Count & Last_Col).End(xlUp).Row
If Cells(last_row, Last_Col).Value Like "*Downloaded*" Then
Cells(last_row, Last_Col).ClearContents
End If
End With
Next R
Data is being drained from another worksheets. For 2 columns where I experience an error, I manually deleted values and inserted another, random batch of values and code worked as intended.
Checked columns formatting, worksheets from which data is taken but I struggle to find a solution.
Thank you for your help.
Clear Last Cell If Criteria Is Met
The main mistake was using Cells(.Rows.Count & Last_Col), where .Rows.Count & Last_Col would have resulted in a 8 or 9-digit string, while it should have been ws.Cells(ws.Rows.Count, Last_Col).End(xlUp).Row which was pointed out by chris neilsen in the comments.
Another important issue is using ws. in front of .cells, .rows, .columns, .range, aka qualifying objects. If you don't do it and e.g. the wrong worksheet is active, you may get unexpected results.
There is no need for looping backwards unless you are deleting.
Although it allows wild characters (*, ?), the Like operator is case-sensitive (a<>A) unless you use Option Compare Text.
The first solution, using the End property, will fail if a number of last columns is hidden or if you insert a new first row e.g. for a title.
The second solution, using the Find method (and the first solution), may fail if the data is filtered.
The Code
Option Explicit
Sub clearLastEnd()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Sheet1")
Dim LastCol As Long
LastCol = ws.Cells(1, ws.Columns.Count).End(xlToLeft).Column
Dim LastRow As Long
Dim c As Long
For c = 1 To LastCol
LastRow = ws.Cells(ws.Rows.Count, c).End(xlUp).Row
With ws.Cells(LastRow, c)
If InStr(1, .Value, "Downloaded", vbTextCompare) > 0 Then
.ClearContents
End If
End With
Next c
End Sub
Sub clearLastFind()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Sheet1")
Dim cel As Range
Set cel = ws.Cells.Find(What:="*", _
LookIn:=xlFormulas, _
SearchOrder:=xlByColumns, _
SearchDirection:=xlPrevious)
If Not cel Is Nothing Then
Dim c As Long
For c = 1 To cel.Column
Set cel = Nothing
Set cel = ws.Columns(c).Find(What:="*", _
SearchDirection:=xlPrevious)
If Not cel Is Nothing Then
If InStr(1, cel.Value, "Downloaded", vbTextCompare) > 0 Then
cel.ClearContents
Else
' The current last non-empty cell does not contain criteria.
End If
Else
' Column is empty.
End If
Next c
Else
' Worksheet is empty.
End If
End Sub
EDIT:
So you are curious why it worked at all. The following should shed a light on it:
Sub test()
Dim i As Long
Debug.Print "Right", "Wrong", "Rows.Count & i"
For i = 1 To 32
Debug.Print Cells(Rows.Count, i).Address, _
Cells(Rows.Count & i).Address, Rows.Count & i
Next i
End Sub
In a nutshell, Cells can have 1 or 2 arguments. When 1 argument is used, it refers to the n-th cell of a range, and it 'counts' by row. The more common usage is with 2 arguments: rows, columns. For example:
Cells(5, 10) ' refers to cell `J5`.
Using one argument is inconvenient here:
Cells(16384 * (5-1) + 10)
i.e.
Cells(65546)
It may be convenient when processing a one-column or a one-row range.
Well , let me see if i understand you have a table in worksheet table have 32 columns and X rows (because you only put WS and i can know if is WS=worksheet or WS= Table-range)
for this i am going to say is selection (if you put worksheet only hace to change for it)
in your code put:
Last_Col = ws.Cells(1, ws.Columns.Count).End(xlToLeft).Column
but in this you always wil obtein 1st cell so i dont understand why exist?
WS.columns.count
return number of columns you selection have
.End(xlToLeft)
return last cell if star to move to left (like Ctrl - left key)
so
Last_Col ---first go to cells (1,ws.Columns.Count) then go to left (End(xlToLeft)) and the end return number of column where finish (.Column) in this case you always get cell (1,"first column of your table")
NOTE: because you wrote that you have allways value in your cells (I have 32 columns with data (various number of rows in columns)
And for Row you have same question
Then you Wrote you want "Delete" but in your code you put Erase value (.ClearContents) so what do you want? because both are no equal
BUT if you have a table and want to search in any cells that have "Download" and only want to "clear content" you just may to use ".find" instead; or if you want to do all at same time you can use .replace (need to check before if .find return "nothing" or no , because if return nothing you get error)
If you have a table with 32 columns and each row have one cell where you put "Donloaded" and want to "delete" all row your code only need select column where appear "downloaded" (example Column "status").
If you have a table where any cell can take value "downloaded" and want to "delete" that cell you need to take care to resize your table and "move to" (when you delete cells you need to say where you want to move yor data remain "letf, "rigth", "up", down).
However if you say that "Downloaded" always appear in last row you can use For to change for all columns and use .end(xlDown)
For i=1 to 32
if cells(1,i).end(xlDown).value="downloaded" then cells(1,i).end(xlDown).ClearContents
next
BUT you need put more information because if you cant garantize that all cells have values and exist cells with "nothing" you will need

How to copy/paste the formulas of Row 2 into lower rows IF Column A isn't empty?

I have a sheet where the cells in Column A auto-populate based on user input. Row 1 is the Headers. Row 2 is fully setup from B:JG with formulas as an example. I would like to have a button that runs a script to check Column A of each row, starting with 3, to see if its empty. If Column A is not empty, it should copy the FORMULAS from B2:JG2 and paste them into Columns B:JG on each row. If Column A is empty, I want it to leave the other columns blank.
I'm just diving into VBA, so any help with a script to accomplish is appreciated.
Example: Rows 3-110 have data in Column A, so B2:JG2 FORMULAS get copied into their B:JG columns. All rows after 110 get nothing because Column A is empty.
The button is on a sheet called "HexBox" and the sheet I need to update is "HexClean".
The user enters some info on the "HexBox" sheet and A:A is auto-populated based on their answers. So there could be 10 or 1000 rows in A:A with values and the rest up to 5000 will be "" if not applicable.
This approach simply
Copies the formulas from 2nd row down to the last used row as determined by Column A (one operation). Note that this step is indifferent of blanks in your column. That is handled in the following 2 steps
Loops through Column A and gather up instances of blank rows by adding them to a Union (collection of cells) (0 operations)
Clears the contents of the Union that is built in step 2 (one operation)
This is a more effecient way to go. Copying & pasting the formulas inside your loop one row at a time will lead to a lot of spread sheet operations. This method has a max of 2 operations
Sub HexSub()
Dim ws As Worksheet: Set ws = ThisWorkbook.Sheets("HexClean")
Dim LR As Long, i As Long, ClearMe As Range
LR = ws.Range("A" & ws.Rows.Count).End(xlUp).Row
ws.Range("B2:JG2").Copy
ws.Range(ws.Cells(3, "B"), ws.Cells(LR, "JG")).PasteSpecial xlPasteFormulas
For i = 3 To LR
If ws.Range("A" & i) = "" Then
If Not ClearMe Is Nothing Then
Set ClearMe = Union(ClearMe, ws.Range("A" & i))
Else
Set ClearMe = ws.Range("A" & i)
End If
End If
Next i
If Not ClearMe Is Nothing Then ClearMe.EntireRow.ClearContents
End Sub
If your range will never have blanks followed by more values, then you can just get rid of the loop and everything below it
If the non-blank cells in column A are typed values then SpecialCells should be able to find them quickly.
Sub populateBelow()
Dim frng As Range
With Worksheets("sheet3")
Set frng = .Range(.Cells(2, "B"), .Cells(2, "JG"))
With .Range(.Cells(3, "A"), .Cells(.Rows.Count, "A").End(xlUp))
With .SpecialCells(xlCellTypeConstants, xlTextValues + xlNumbers)
frng.Copy Destination:=.Offset(0, 1)
End With
End With
End With
End Sub
Sub CopyToMany()
Dim xRow As Range, aCel As Range
For Each xRow In ActiveSheet.UsedRange.Rows
If xRow.Row > 2 Then
Set aCel = xRow.Cells(1, 1)
If aCel.Value <> "" Then
ActiveSheet.Range("B2:JG2").Copy Destination:=ActiveSheet.Range("B" & xRow.Row & ":JG" & xRow.Row)
End If
End If
Next xRow
End Sub

Select all data in a column with variable number of rows

I have the example where I want to write a VBA statement which will select all data in a single column, there are no blanks in the column data. The column position will never change e.g. column A, and the data starts in row 3. However the total number of rows in the column will change regularly.
I want the system to dynamically select all the cells in column and then I can run a method against these selected pieces of data.
As an example of performing an action on your range without selecting it:
Public Sub Test()
Dim rColA As Range
With ThisWorkbook.Worksheets("Sheet1")
Set rColA = .Range(.Cells(3, 1), .Cells(.Rows.Count, 1).End(xlUp))
MsgBox "Column A range is " & rColA.Address 'Delete if you want.
rColA.Interior.Color = RGB(255, 0, 0) 'Turn the back colour red.
rColA.Cells(2, 1).Insert Shift:=xlDown 'Insert a blank row at second cell in range
'So will insert at A4.
'If the first cell in your range is a number then double it.
If IsNumeric(rColA.Cells(1, 1)) Then
rColA.Cells(1, 1) = rColA.Cells(1, 1) * 2
End If
End With
End Sub
Try
Dim LastRow as Long, sht as worksheet
Set sht = ThisWorkbook.Worksheets("My Sheet Name")
LastRow = sht.Cells(sht.Rows.Count, 1).End(xlUp).Row
sht.Range("A3:A" & LastRow).Select
Like Darren Bartrup-Cook says, you may not need to select the data, you can almost always perform actions directly which is much faster.
If your column is "isolated" meaning no other nonblank cells touch your data you can use:
Range("firstCellInYourColumn").CurrentRegion.Select
(this works the same way as Ctrl+* from keyboard)
otherwise use:
Range(Range("firstCellInYourColumn"), Range("firstCellInYourColumn").End(xlDown)).Select
both will work if there are really no blanks within your data.
You should also prepend all Range with worksheet expression, I omitted this.

Excel VBA - Trying to return values that that only contain all criteria

Okay so this one is hard to explain - I have a very large table that has customers, part numbers, price, and revenue. I need to return all customers that use a list of part numbers; so for instance if they use parts ABC and DEF then it would return the customers that use those parts, and the revenue for those customers (I figured I would copy the entire rows to another table or something).
I don't want to see customers that use one part but not the other. I've tried doing autofilters and advanced filters with no luck, but I would rather do this in VBA if possible. I'm not sure which way would be the easiest...
One thought was to pivot the table and sort by customers, but this is very manual and I need to pull these results into another table so I can see the data separately. Any help is much appreciated!
Example table
edited after OP's clarification. see added code
You can use the "xlFilterValues" operator of "AutoFilter()" method of "Range" object.
Assuming first row has headers, here's the "basic concepts" code you asked for:
Dim partListArr As Variant
With Worksheets("MyListSheetName")
partListArr = Application.Transpose(.Range("A1", .Cells(.Rows.Count,1).End(xlUp)).Value)'<--| retrieve the content of its column A cells from row 1 down to its last not empty cell
End With
With Worksheets("MyDataSheetName")
With .Range("Z1", .Cells(.Rows.Count,1).End(xlUp)) '<--| reference its A to Z columns cells from row 1 down to column A last not empty cell
.Autofilter field:=2, Criteria1:=partListArray, operator:=xlFilterValues '<--| filter referenced range on its 2nd field with list of parts
With .Resize(.Rows.Count - 1).Offset(1).SpecialCells(xlCellTypeVisible) '<--| reference filtered cells, skipping headers
' here your code to handle filtered cells
End With
End With
End With
Since your clarifications you could still use nested AutoFilter()s to catch proper customers sharing all listed parts, but it's more effective leaving this work to dictionaries and use AutoFilter() for the final copy/paste part. like follows:
Option Explicit
Sub main()
Dim custDict As Scripting.Dictionary, partDict As Scripting.Dictionary
Dim cust As Variant, part As Variant
Dim parts As String
Dim okCust As Boolean
With Worksheets("MyListSheetName")
Set partDict = GetList(.Range("A1", .Cells(.Rows.count, 1).End(xlUp)))
End With
With Worksheets("MyDataSheetName")
With .Range("Z1", .Cells(.Rows.count, 1).End(xlUp)) '<--| reference its A to Z columns cells from row 1 down to column A last not empty cell
Set custDict = GetList(.Resize(.Rows.count, 1).Offset(1))
For Each cust In custDict.Keys
parts = custDict(cust) & "|"
For Each part In partDict.Keys
okCust = InStr(parts, "|" & part & "|") > 0
If Not okCust Then Exit For
Next part
If okCust Then
.AutoFilter field:=1, Criteria1:=cust
With .Resize(.Rows.count - 1).Offset(1).SpecialCells(xlCellTypeVisible) '<--| reference filtered cells, skipping headers
.Copy Destination:=GetSheet(CStr(cust)).Range("A1")
End With
End If
Next cust
End With
.AutoFilterMode = False
.Activate
End With
End Sub
Function GetList(rng As Range) As Scripting.Dictionary
Dim dict As New Scripting.Dictionary
Dim cell As Range
For Each cell In rng.Cells
dict(cell.Value) = dict(cell.Value) & "|" & cell.Offset(, 1)
Next cell
Set GetList = dict
End Function
Function GetSheet(shtName As String) As Worksheet
On Error Resume Next
Set GetSheet = Worksheets(shtName)
If GetSheet Is Nothing Then
Set GetSheet = Worksheets.Add
GetSheet.Name = shtName
Else
GetSheet.UsedRange.ClearContents
End If
End Function

Resources