Count unique values in column based on another column (Microsoft Excel 2013) - excel

For a project, I'm creating an Excel macro to count unique column values based on another column value. Here is a basic example of the macro I'm trying to create:
Data
col_1
col_2
a
x
a
y
b
z
b
z
Macro
Sub Main()
Dim Param As String
Param = "a"
MsgBox UniqueValues(Param)
End Sub
Function UniqueValues(Param As String) As String
Dim EvaluateString As String
EvaluateString = "=SUM(--(LEN(UNIQUE(FILTER(B:B,A:A=" & """" & Param & """" & ","""")))>0))"
UniqueValues = Evaluate(EvaluateString)
End Function
Expectation
The expectation is that for Param = "a" the function returns 2 and for Param = "b" it returns 1.
Issue
Even though function works perfpectly in Excel for Microsoft 365 Apps for Enterprise, the project requires me to use Excel for Microsoft Office Standard 2013. This version doesn't support the use of the UNIQUE and FILTER functions used in EvaluateString.
I want to understand if there's a simple way to count the unique values in a column based on a value in another column in Excel for Microsoft Office Standard 2013. Your help is much appreciated.

You can use the array formula
=SUM(IF($A$1:$A$5="a",1/COUNTIFS($A$1:$A$5,"a",$B$1:$B$5,$B$1:$B$5)),0)
After entering the formula, instead of Enter, you need to press Ctl + Shift + Enter
In VBA, the above formula can be used as shown below
Option Explicit
Sub Main()
Dim Param As String
Param = "b"
MsgBox "The count for " & Param & " is " & UniqueValues(Param)
End Sub
Function UniqueValues(Param As String) As String
Dim EvaluateString As String
Dim ws As Worksheet
'~~> Change this to the relevant worksheet
Set ws = Sheet1
'SUM(IF(Sheet1!A1:A5="a",1/COUNTIFS(Sheet1!A1:A5,"a",Sheet1!B1:B5,Sheet1!B1:B5)),0)
EvaluateString = "SUM(IF($A$1:$A$5=" & _
Chr(34) & Param & Chr(34) & _
",1/COUNTIFS($A$1:$A$5," & _
Chr(34) & Param & Chr(34) & _
",$B$1:$B$5," & _
"$B$1:$B$5)),0)"
UniqueValues = ws.Evaluate(EvaluateString)
End Function
In Action

When your data are in "Sheet1", columns A and B, starting in row 1, you can use this macro (results in columns D and E):
Sub macro1()
Dim a As Integer, p As Integer, x As Integer, y As Integer
a = 0: p = 0: x = 1: y = 1
With Sheets("Sheet1")
.Columns("d:e").ClearContents
Do Until x > .Cells(.Rows.Count, 1).End(xlUp).Row
a = 1
Do While .Cells(x, 1) = .Cells(y, 1)
If .Cells(x, 2) <> .Cells(y, 2) Then a = a + 1
x = x + 1
Loop
p = p + 1
.Cells(p, 4) = .Cells(y, 1)
.Cells(p, 5) = a
y = x
Loop
End With
End Sub

Related

Alternative of FILTER function on non office 365

Using a Macro or Formula, is there a way to achieve the result of the following formula of Office 365?
=FILTER(B:B,A:A = "x")
What it does is get all the values from Column B if Column A on the same row has a value of x.
My PC has office 365 but the one I'm working with only has Office Pro Plus 2019. I had to use my pc when I needed the function and I'm getting tired of it, maybe it can be done on Office Pro Plus 2019 too using a formula or a macro?
Use:
=IFERROR(INDEX($B$1:$B$100,AGGREGATE(15,7,ROW($A$1:$A$100)/($A$1:$A$100="x"),ROW($ZZ1))),"")
Note the use of a set range and not full columns. That is done on purpose, This being an array formula it will do a lot of calculations each cell it is placed. Limiting the range to the data set will speed it up.
Put this in the first cell of the output and copy down till blanks are returned.
I had some spare time and I am recently interested in User defined functions so I decided to make my own version of what I imagine this would be. I'm prefacing this by saying its not good and is excessively long but it works!
Function JOINIF(ByRef IfRange As Range, ByVal Criteria As String, Optional JoinRange As Range, Optional Delimeter As String = ",") As String
'IfRange is the range that will be evaluated by the Criteria
'Criteria is a logical test that can be applied to a cell value.
'Examples of Criteria: "=Steve", ">100", "<>Toronto", "<=-1"
'JoinRange is the range of values that will be concatenated if the corresponding -
'IfRange cell meets the criteria. JoinRange can be left blank if the values to be -
'concatenated are the IfRange values.
'Delimeter is the string that will seperate the concatenated values.
'Default delimeter is a comma.
Dim IfArr() As Variant, JoinArr() As Variant, OutputArr() As String
Dim IfArrDim As Integer, JoinArrDim As Integer
Dim JCount As Long, LoopEnd(1 To 2) As Long
Dim MeetsCriteria As Boolean, Expression As String
Dim i As Long, j As Long
'PARSING THE CRITERIA
Dim Regex As Object
Set Regex = CreateObject("VBScript.RegExp")
Regex.Pattern = "[=<>]+"
'Looking for comparison operators
Dim Matches As Object
Set Matches = Regex.Execute(Criteria)
If Matches.Count = 0 Then
'If no operators found, assume default "Equal to"
If Not IsNumeric(Criteria) Then
'Add quotation marks to allow string comparisons
Criteria = "=""" & Criteria & """"
End If
Else
If Not IsNumeric(Replace(Criteria, Matches(0), "")) Then
Criteria = Matches(0) & """" & Replace(Criteria, Matches(0), "") & """"
End If
'Add quotation marks to allow string comparisons
End If
'Trim IfRange to UsedRange
Set IfRange = Intersect(IfRange, IfRange.Parent.UsedRange)
'Default option for optional JoinRange input
If JoinRange Is Nothing Then
Set JoinRange = IfRange
Else
Set JoinRange = Intersect(JoinRange, JoinRange.Parent.UsedRange)
End If
'DIMENSIONS
'Filling the arrays
If IfRange.Cells.Count > 1 Then
IfArr = IfRange.Value
IfArrDim = Dimensions(IfArr)
Else
ReDim IfArr(1 To 1)
IfArr(1) = IfRange.Value
IfArrDim = 1
End If
If JoinRange.Cells.Count > 1 Then
JoinArr = JoinRange.Value
JoinArrDim = Dimensions(JoinArr)
Else
ReDim JoinArr(1 To 1)
JoinArr(1) = JoinRange.Value
JoinArrDim = 1
End If
'Initialize the Output array to the smaller of the two input arrays.
ReDim OutputArr(IIf(IfRange.Cells.Count < JoinRange.Cells.Count, IfRange.Cells.Count - 1, JoinRange.Cells.Count - 1))
'DEFINING THE LOOP PARAMETERS
'Loop ends on the smaller of the two arrays
If UBound(IfArr) > UBound(JoinArr) Then
LoopEnd(1) = UBound(JoinArr)
Else
LoopEnd(1) = UBound(IfArr)
End If
If IfArrDim = 2 Or JoinArrDim = 2 Then
If Not (IfArrDim = 2 And JoinArrDim = 2) Then
'mismatched dimensions
LoopEnd(2) = 1
ElseIf UBound(IfArr, 2) > UBound(JoinArr, 2) Then
LoopEnd(2) = UBound(JoinArr, 2)
Else
LoopEnd(2) = UBound(IfArr, 2)
End If
End If
'START LOOP
If IfArrDim = 1 Then
For i = 1 To LoopEnd(1)
If IsNumeric(IfArr(i)) And IfArr(i) <> "" Then
Expression = IfArr(i) & Criteria
Else
'Add quotation marks to allow string comparisons
Expression = """" & IfArr(i) & """" & Criteria
End If
MeetsCriteria = Application.Evaluate(Expression)
If MeetsCriteria Then
If JoinArrDim = 1 Then
OutputArr(JCount) = CStr(JoinArr(i))
Else
OutputArr(JCount) = CStr(JoinArr(i, 1))
End If
JCount = JCount + 1
End If
Next i
Else
For i = 1 To LoopEnd(1)
For j = 1 To LoopEnd(2)
If IsNumeric(IfArr(i, j)) And IfArr(i, j) <> "" Then
Expression = IfArr(i, j) & Criteria
Else
'Add quotation marks to allow string comparisons
Expression = """" & IfArr(i, j) & """" & Criteria
End If
MeetsCriteria = Application.Evaluate(Expression)
If MeetsCriteria Then
If JoinArrDim = 1 Then
OutputArr(JCount) = CStr(JoinArr(i))
Else
OutputArr(JCount) = CStr(JoinArr(i, j))
End If
JCount = JCount + 1
End If
Next j
Next i
End If
'END LOOP
ReDim Preserve OutputArr(JCount + 1 * (JCount > 0))
JOINIF = Join(OutputArr, Delimeter)
End Function
Private Function Dimensions(var As Variant) As Long
'Credit goes to the great Chip Pearson, chip#cpearson.com, www.cpearson.com
On Error GoTo Err
Dim i As Long, tmp As Long
While True
i = i + 1
tmp = UBound(var, i)
Wend
Err:
Dimensions = i - 1
End Function
Examples of it in use:
Seperate IfRange and JoinRange
IfRange as the JoinRange
You might try the following udf (example call: FILTER2(A1:A100,B1:B100)) consisting of the following tricky steps:
a) Evaluate the general condition (=If(A1:A100="x",Row(A1:A100),"?") as tabular Excel formula and assign all valid row numbers to array x (marking the rest by "?" strings),
b) Filter out all "?" elements
c) Apply x upon the data column benefitting from the advanced restructuring features of Application.Index()
Public Function Filter2(rng1 As Range, rng2 As Variant, Optional ByVal FilterID As String = "x")
Dim a As String: a = rng1.Address(False, False, External:=True)
'a) get all valid row numbers (rng1)
Dim myformula As String: myformula = "if(" & a & "=""" & FilterID & """,row(" & a & "),""?"")"
Dim x: x = Application.Transpose(Evaluate(myformula))
'b) filter out invalid "?" elements
x = VBA.Filter(x, "?", False)
'c) apply x upon data column (rng2)
If UBound(x) > -1 Then Filter2 = Application.Index(rng2, Application.Transpose(x), 1)
End Function
Note that function calls before versions 2019/MS 365 need to be entered as array formula (Ctrl+Shift+Enter).
The function assumes one-column (range) arguments.
Edit due to comment as of 2022-06-08
The whole example is based on the actual row numbers starting in the first row (OP ranges refer to A:A,B:B. If you want to allow ranges to start at any row, you'd need to change the myFormula definition in section a) by correcting the row indices by subtracting possible offsets (row number + 1 - first row):
Dim myFormula As String
myFormula = "if(" & a & "=""" & FilterID & """,row(" & a & ")+1 -" & rng1.Row & ",""?"")"
Try this UDF for the Filter Function:
Function FILTER_HA(Where, Criteria, Optional If_Empty) As Variant
Dim Data, Result
Dim i As Long, j As Long, k As Long
'Create space for the output (same size as input cells)
With Application.Caller
i = .Rows.Count
j = .Columns.Count
End With
'Clear
ReDim Result(1 To i, 1 To j)
For i = 1 To UBound(Result)
For j = 1 To UBound(Result, 2)
Result(i, j) = ""
Next
Next
'Count the rows to show
For i = 1 To UBound(Criteria)
If Criteria(i, 1) Then j = j + 1
Next
'Empty?
If j < 1 Then
If IsMissing(If_Empty) Then
Result(1, 1) = CVErr(xlErrNull)
Else
Result(1, 1) = If_Empty
End If
GoTo ExitPoint
End If
'Get all data
Data = Where.Value
'Copy the rows to show
For i = 1 To UBound(Data)
If Criteria(i, 1) Then
k = k + 1
For j = 1 To UBound(Data, 2)
Result(k, j) = Data(i, j)
Next
End If
Next
'Return the result
ExitPoint:
FILTER_HA = Result
End Function

Removing all data before the first '-' in a column in VBA

My spreadsheet currently has a column C with rows of data that have this structure below:
123 - abc - xyz
I want my VBA code to remove all the data before the first - including the - so that the column C would look like this:
abc - xyz
My current code is removing both "-"
Sub TrimCell()
Dim i As String
Dim k As String
i = "-"
k = ""
Columns("C").Replace what:=i, replacement:=k, lookat:=xlPart,
MatchCase:=False
End Sub
The Excel function I have for this is =REPLACE(C1,1,FIND("-",C1),""). This works but I want something in VBA.
This will work on column C:
Sub my_sub()
Dim c As Range
For Each c In Intersect(ActiveSheet.UsedRange, ActiveSheet.Range("C:C"))
c = Trim(Mid(c, InStr(c, "-") + 1))
Next
End Sub
You want to find the location of the first "-"
location = instr(1, cells(iRow,3), "-", vbTextCompare)
Taking advantage of fact that instr only returns the first entry...
Then trim the cell to the right using that location
if location > 0 then
'Found a "-" within this cell
cells(iRow,3) = right(cells(iRow,3), len(cells(iRow,3)-location)
end if
iRows is obviously just my iterator over the rows in your data. Define it whatever way you want.
You could dot it in one go using Evaluate.
With Range("C1", Range("C" & Rows.Count).End(xlUp))
.Value = Evaluate("MID(" & .Address & ", FIND(""-"", " & .Address & ")+1, LEN(" & .Address & "))")
End With
Please, try the next function:
Function replaceFirstGroup(x As String) As String
Dim arr
arr = Split(x, " - ")
arr(0) = "###$"
replaceFirstGroup = Join(Filter(arr, "###$", False), " - ")
End Function
It can be called/tested in this way:
Sub testReplaceFirstGroup()
Dim x As String
x = "123 - abc - xyz"
MsgBox replaceFirstGroup(x)
End Sub
In order to process C:C column, using the above function, please use the next code. It should be extremely fast using an array, working in memory and dropping the processing result at once:
Sub ProcessCCColumn()
Dim sh As Worksheet, lastR As Long, arr, i As Long
Set sh = ActiveSheet
lastR = sh.Range("C" & sh.rows.count).End(xlUp).row
arr = sh.Range("C2:C" & lastR).value
For i = 1 To UBound(arr)
arr(i, 1) = replaceFirstGroup(CStr(arr(i, 1)))
Next i
sh.Range("C2").Resize(UBound(arr), 1).value = arr
End Sub

Using wildcards in Excel VBA

I am trying to use wildcards in a formula to count cells in a table column which contain text and not ""
I tried the following methods:
String comparison
Dim g As Integer
g = Application.WorksheetFunction.CountIf(ThisWorkbook.Worksheets("ws1").ListObjects("Table1").ListColumns("ColumnA").DataBodyRange, ""*?"")
Using a tilde failed:
Dim g As Integer
g = Application.WorksheetFunction.CountIf(ThisWorkbook.Worksheets("ws1").ListObjects("Table1").ListColumns("ColumnA").DataBodyRange, ""~*?"")
Using ASCII characters below returned 0:
g = Application.WorksheetFunction.CountIf(ThisWorkbook.Worksheets("ws1").ListObjects("Table1").ListColumns("Column1").DataBodyRange, Chr(34) & Chr(63) & Chr(42) & Chr(34))
Tried and tested:
Public Function not_qt(ByVal rng As Range) As Integer
Dim cell As Range
Dim counter As Integer: counter = 0
For Each cell In rng
If Not IsEmpty(cell) Then
If Not cell Like Chr(34) & "*" & Chr(34) Then
counter = counter + 1
'cell.Offset(0, 1) = counter '<- Only for illustration purposes
End If
End If
Next cell
not_qt = counter
End Function

Adjusting a Vlookup according to a combo box value

I have a system which inputs a code to a cell on a spreadsheet. It does this by using a Vlookup to determine which date it is. If you look at the code below the nput is what does this Vlookup.
What I want it to do is move down a cell per amount the amount that will be in a combo box value called DayAmount. What would I need to enter for it to look at the next cell?
For example if the 5th of January is in A24 I want it to also enter the same code in the 6th and 7th of January which the Vlookup knows is A25 and A26.
Private Sub Submitplan_Click()
' This searches for the selected engineer
Dim EngineerFound As Range
Dim Front As Worksheet
Dim wb As Workbook
Dim area As Worksheet
Set wb = ThisWorkbook
Dim tabchange As String
Set Front = wb.Worksheets("Front")
x = Front.Cells(Front.Rows.Count, "F").End(xlUp).Row
With Front.Range("F8:F" & x)
Set EngineerFound = .Find(Engbox.Value, LookIn:=xlValues)
End With
EngRow = EngineerFound.Row
'This is the section which enters the data into the right date
tabchange = ("Area") & Front.Range("B8")
Set area = wb.Worksheets(tabchange)
y = WorksheetFunction.VLookup(CLng(CDate(Datebox.Value)), area.Range("A:B"), 2, 0)
nPut = WorksheetFunction.VLookup(Key, area.Range("A:B"), 2, 0) &
Hoursbox.Value
z = area.Range("C:C").Find(Engbox.Value).Row
If area.Cells(z, y).Value = " B/H" Then
area.Cells(z, y).Value = nPut & " " & "B/H"
ElseIf area.Cells(z, y).Value = " WK" Then
area.Cells(z, y).Value = nPut & " " & "WK"
Else: area.Cells(z, y).Value = nPut
End If
' If DayAmount <> "" Then
'End If
Call Update
Unload Me
End Sub
If I'm reading this correctly, you have a value in a combobox (will say DayAmount) which will be assigned until a that value is met.
Dim i as Long, j as Long, k as Long
i = ActiveCell.Row
j = DayAmount
k = 1
If j > 1 Then
Do until k = j-1
Cells(k+1,1).Value = Cells(i,1)>Value
k = i + k
Loop
End If
Or you could use a filldown, or .value match, and when you enter the line to the destination cell, you use:
Dim i as Long, j as Long
i = ActiveCell.Row
j = DayAmount
Range(Cells(i,1),Cells(i+j,1)).Value = "" 'input here
Note the arbitrary activecell and column 1 usage as i'm unsure exactly where this would be for you.
Regarding, specifically, the use of nPut, you can use offset to help, such as:
Range(nPut, nPut.Offset(DayAmount,0)) = WorksheetFunction.VLookup(Key, area.Range("A:B"), 2, 0) & Hoursbox.Value
Note that I haven't tested the latter and it's off the top of my head.

UDF to concatenate values

I am trying to build a user defined function using VBA for excel. That would concatenate a list of stores which has a x mark in that row.
Store1 Store2 Store3 Concatenate
x x Store1,Store3
x x tore1,Store2
x Store1
I managed to write this vba code, but I am not sure this is the best approach. As I was tesing in on 1000 and more lines, it was quite slow. Maybe it is possible to optimise it?
firstStore you point where the first store starts (not the names, but the x marks,lastStore1 the last column. listofstores1 is the row where the store names are.
Function listofstores(firstStore As Range, lastStore1 As Range, listofstores1 As Range)
Application.Volatile
Dim offsetvalue As Integer
offsetvalue = -(lastStore1.Row - listofstores1.Row)
lastStore = lastStore1.Column
Set initial = firstStore
For i = 1 To lastStore
If initial = "X" Or initial = "x" Then Store = initial.Offset(offsetvalue, 0)
c = 1
Set initial = initial.Offset(0, c)
listofstores = listofstores & " " & Store
Store = ""
Next i
End Function
Short but intricate.
uses Evaluate to return an array of matches (Store numbers v x)
Filter removes the non-matches ("V")
Join to make the string from the final array of matches
UDF
Function Getx(Rng1 As Range, Rng2 As Range) As String
Getx = Join(Filter(Evaluate("=ÏF(" & Rng2.Address & "=""x""," & Rng1.Address & ",""V"")"), "V", False), ",")
End Function
Another way to achieve is as below. You can do any where in sheets
Sub Main()
Call getlistofstores(Range("G13:L15"), Range("G12:L12"))
End Sub
Function getlistofstores(stores As Range, listofstores As Range)
Application.Volatile
Dim fullconcatstring As String
Dim row As Integer
Dim column As Integer
a = stores.Count / listofstores.Count
b = listofstores.Count
row = stores.Cells(1).row
column = stores.Cells(1).column + (b)
For i = 1 To a
For j = 1 To b
If stores.Cells(i, j) = "x" Then
If concatstring <> "" Then
concatstring = concatstring & ", " & listofstores.Cells(j)
Else
concatstring = listofstores.Cells(j)
End If
End If
Next j
fullconcatstring = fullconcatstring & Chr(10) & Chr(11) & concatstring
concatstring = ""
Next i
Call concatenateallstores(row, column, fullconcatstring)
End Function
Sub concatenateallstores(r As Integer, c As Integer, d As String)
str1 = Split(d, Chr(10) & Chr(11))
str2 = UBound(str1)
For i = 1 To str2
Cells(r, c) = str1(i)
r = r + 1
Next i
End Sub

Resources