COUNTIFS VLOOKUP returns a value? - excel

I'm stuck for a formula. Essentially what i want to do is count the number of times a particular value appears in one sheet, based on data pulled from another sheet.
E.g. I have three sheets. One sheet has a list of jobs and a code associated with a client. The second sheet has a list of the clients and details on the client. The third sheet is my results sheet.
I want to count the How Heards for each company. Eg, in sheet 1 below Apple has 3 customers. If we use the Client Code Id's and go to Sheet 2, we can see that it will total 2 Online and 0 Facebook. This result displays on Sheet 3. The results sheet.
Sheet 1 Example
Sheet 2 Example
Sheet 3 Example (What i want the results to look like from calculation)

In the interest of actually providing a suitable answer to this question, here is some VBA code that allows a workbook to create an ADO connection to itself and generate a report using SELECT, DISTINCT, WHERE, INNER JOIN, GROUP BY and ORDER BY clauses.
Sub Inner_Join()
Dim cnx As Object, rs As Object
Dim sWS1 As String, sWS2 As String, sWB As String, sCNX As String, sSQL As String
Dim ws1TBLaddr As String, ws2TBLaddr As String
'Collect some string literals that will be used to build SQL
ws1TBLaddr = Worksheets("Sheet1").Cells(1, 1).CurrentRegion.Address(0, 0)
sWS1 = Worksheets("Sheet1").Name
ws2TBLaddr = Worksheets("Sheet2").Cells(1, 1).CurrentRegion.Address(0, 0)
sWS2 = Worksheets("Sheet2").Name
sWB = ThisWorkbook.FullName
'Build the connection string
'The first is for 64-bit Office; the second is more universal
sCNX = "Provider=Microsoft.Jet.OLEDB.12.0;Data Source=" & sWB _
& ";Extended Properties=""Excel 8.0;HDR=Yes;IMEX=1"";"
sCNX = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & sWB _
& ";Extended Properties=""Excel 8.0;HDR=Yes;IMEX=1"";"
'Debug.Print sCNX
'Create the necessary ADO objects
Set cnx = CreateObject("ADODB.Connection") 'late binding; for early binding add
Set rs = CreateObject("ADODB.Recordset") 'Microsoft AxtiveX Data Objects 6.1 library
'Open the connection to itself
cnx.Open sCNX
With Worksheets("Sheet3")
'Clear the reporting area
.Cells(1, 1).CurrentRegion.ClearContents
'get [Business Name] list from Sheet1
sSQL = "SELECT DISTINCT w1.[Business Name]"
sSQL = sSQL & " FROM [" & sWS1 & "$" & ws1TBLaddr & "] w1"
sSQL = sSQL & " ORDER BY w1.[Business Name]"
'Debug.Print sSQL
'Populate Sheet3!A:A
rs.Open sSQL, cnx
Do While Not rs.EOF
'Debug.Print rs.Fields("Business Name")
.Cells(.Rows.Count, 1).End(xlUp).Offset(1, 0) = rs.Fields("Business Name")
rs.MoveNext
Loop
rs.Close
'get [How Heard] list from Sheet2
sSQL = "SELECT DISTINCT w2.[How Heard]"
sSQL = sSQL & " FROM [" & sWS2 & "$" & ws2TBLaddr & "] w2"
sSQL = sSQL & " WHERE w2.[How Heard] NOT LIKE 'None'"
sSQL = sSQL & " ORDER BY w2.[How Heard]"
'Debug.Print sSQL
'Populate Sheet3!1:1
rs.Open sSQL, cnx
Do While Not rs.EOF
'Debug.Print rs.Fields("How Heard")
.Cells(1, .Columns.Count).End(xlToLeft).Offset(0, 1) = rs.Fields("How Heard")
rs.MoveNext
Loop
rs.Close
'start by seeding zeroes for all
With .Cells(1, 1).CurrentRegion
With .Resize(.Rows.Count - 1, .Columns.Count - 1).Offset(1, 1)
.Cells = 0
End With
End With
'get the counts for the [Business Name]×[How Heard] combinations
sSQL = "SELECT COUNT(w1.[Business Name]), w1.[Business Name], w2.[How Heard]"
sSQL = sSQL & " FROM [" & sWS1 & "$" & ws1TBLaddr & "] w1"
sSQL = sSQL & " INNER JOIN [" & sWS2 & "$" & ws2TBLaddr & "] w2 ON w1.[Client Code] = w2.[Client Code]"
sSQL = sSQL & " WHERE w2.[How Heard] <> 'None'"
sSQL = sSQL & " GROUP BY w1.[Business Name], w2.[How Heard]"
'Debug.Print sSQL
'Populate Sheet3 data matrix area
rs.Open sSQL, cnx
With .Cells(1, 1).CurrentRegion
Do While Not rs.EOF
'Debug.Print rs.Fields(0) & ":" & rs.Fields(1) & ":" & rs.Fields(2)
.Cells(Application.Match(rs.Fields(1), .Columns(1), 0), _
Application.Match(rs.Fields(2), .Rows(1), 0)) = rs.Fields(0)
rs.MoveNext
Loop
End With
rs.Close
End With
Final_Cleanup:
Set rs = Nothing
cnx.Close: Set cnx = Nothing
End Sub
Results should be similar to the following.
    

Ok, so I am really impressed with the answer by #Jeeped
My answer is not as flexible as being able to use arbitrary SQL but it doesn't use VBA so it might be useful as well in some contexts.
So my answer basically:
creates an array from Sheet1 that contains the client code for each matching cell(or 0 for non matching cells)
X = ((Sheet1!$B$2:$B$1000=$A2)*Sheet1!$A$2:$A$1000)
creates an array from Sheet2 that contains the client code for each matching cell(or 0 for non matching cells)
Y = ((Sheet2!$B$2:$B$2000=B$1)*Sheet2!$A$2:$A$2000)
compares every cell in the two arrays where the value of the first array isn't 0
Z = (X<>0)*(X=TRANSPOSE(Y))
and then sums up the number of matches:
=SUM(Z)
So the final formula for Sheet3!B2 is:
=SUM((((Sheet1!$B$2:$B$1000=$A2)*Sheet1!$A$2:$A$1000)<>0)*(((Sheet1!$B$2:$B$1000=$A2)*Sheet1!$A$2:$A$1000)=TRANSPOSE(((Sheet2!$B$2:$B$2000=B$1)*Sheet2!$A$2:$A$2000))))
It is an array formula so you need to press Control-Shift-Enter instead of just Enter. Then you need to copy it from B2 to C2, B3 and so on.
Obviously, You will have to change the 1000 to something bigger than the largest rwo on Sheet1 and the 2000 to something bigger than the largest row on Sheet2.

Related

Trouble with ADO query from Excel VBA with dates

Example data
Dim strFileName As String
strFileName = "Acquired last 30 days.xlsx"
Dim lDays As Long
lDays = 2
Dim chkDate As Date
chkDate = Date - lDays
Dim cn As Object, cmd As Object, rs As Object
'---Connecting to the Data Source---
Set cn = CreateObject("ADODB.Connection")
Set cmd = CreateObject("ADODB.Command")
With cn
.Provider = "Microsoft.ACE.OLEDB.12.0"
.ConnectionString = "Data Source=" & ThisWorkbook.Path & "\" & strFileName & ";Extended Properties=""Excel 12.0 Xml;HDR=NO;IMEX=1"";"
.Open
End With
' Assemble paramaters and run
With cmd
.ActiveConnection = cn
.CommandType = 1
.CommandText = "SELECT [F1],[F3],[F4] FROM [Sheet 1$] WHERE F4>?"
.Parameters.Append .CreateParameter("pDate4", 7, 1, 30, chkDate)
Set rs = .Execute
End With
If Not rs.EOF Then
Do
Debug.Print rs(0) & "-" & rs(1) & "-" & rs(2)
rs.Movenext
Loop Until rs.EOF
End If
I've tried with and without parameters, with and without #, with and without BETWEEN, with and without using the same date format, with and without CDate() in the the where and select clauses (get an invalue Null error in the where clause - likely because of the blank lines at the top of the table). My basic test results return all the values or none of the values. Some examples:
All values including header:
"SELECT [F1],[F3],[F4] FROM [Sheet 1$] WHERE F4>#2018/01/05#"
"SELECT [F1],[F3],[F4] FROM [Sheet 1$] WHERE F4>#01/05/2018#"
Just header:
"SELECT [F1],[F3],[F4] FROM [Sheet 1$] WHERE F4>#05/01/2018#"
Nothing:
"SELECT [F1],[F3],[F4] FROM [Sheet 1$] WHERE F4 BETWEEN #01/01/2019# AND #01/01/2021#"
The data starts around row 15, and the date in the column 4 is stored as a date in Excel but formatted dd-mmm-yyyy. I'd like to avoid opening the file and reformatting it.
I'm at a loss. The only thing I can think of is to pull all the data and then evaluate it line by line in the recordset before writing, but I'd REALLY like to avoid that as well.
What am I doing wrong????
This worked for me:
With cmd
.ActiveConnection = cn
.CommandType = 1
.CommandText = "SELECT [F1],[F3],[F4] FROM [Sheet1$] WHERE F4 > ?"
.Parameters.Append .CreateParameter("pDate4", 5, 1, 30, CLng(chkDate)) '5=adDouble
Set rs = .Execute
End With
If Not rs.EOF Then
Do
Debug.Print rs(0) & "-" & rs(1) & "-" & Format(rs(2).Value, "mm/dd/yyyy")
rs.Movenext
Loop Until rs.EOF
End If
It's not what I was looking for, but this was the only thing I could get to work.
I ended up writing a loop to put together the strings so that they match the results I was seeing from the recordset. (The column names are slightly different as I paired back the table for my original question, but you get the idea.)
Dim i As Long
lDays = lDays - 1
Dim strDate As String
For i = 0 To lDays
strDate = CStr(Format(Date - i, "dd-mmm-yyyy"))
strTemp = AHK_Append(strTemp, "F9='" & strDate & "' OR F15='" & strDate, "' OR ")
Next
strTemp = " WHERE F8='Acquired' AND (" & strTemp & "')"
I really wish there was a way to convert a value prior to evaluating it in the WHERE clause, but I'm moving on.

Using 'Insert Into' SQL Statement with column names on Excel Worksheet with ACE OLEDB fails

So, I want to get disciplined in how I store data to worksheets and was wanting to use the SQL OLEDB Provide for Excel and standard SQL statements. Insert into with column names does not work, yet, for me at least. Some code demonstrates the problem. Expecting both forms shown here to work W3 Schools SQL INSERT INTO Statement
Option Explicit
Sub MinimalCompleteVerifiableExample()
'Tools->References "Microsoft ActiveX Data Objects 2.8 Library"
Dim wsNew As Excel.Worksheet
Set wsNew = ThisWorkbook.Worksheets.Add
wsNew.Cells(1, 1) = "TimeStamp"
wsNew.Cells(1, 2) = "Path"
Dim oConn As ADODB.Connection
Set oConn = New ADODB.Connection
Debug.Assert UBound(Split(ThisWorkbook.Name, ".")) > 0 '* Workbook needs to be saved
oConn.Open "Provider=Microsoft.ACE.OLEDB.12.0;" & _
"Data Source=" & ThisWorkbook.FullName & ";" & _
"Extended Properties='Excel 12.0 Macro'"
Dim rsTestRead As ADODB.Recordset
Set rsTestRead = New ADODB.Recordset
rsTestRead.Open "Select * from [" & wsNew.Name & "$] AS P", oConn, adOpenStatic
Debug.Assert oConn.Errors.Count = 0
Debug.Assert rsTestRead.Fields.Item(0).Name = "TimeStamp"
Debug.Assert rsTestRead.Fields.Item(1).Name = "Path"
Dim sSQL As String
sSQL = "insert into [" & wsNew.Name & "$] (TimeStamp,Path) VALUES ('31-Dec-2015','C:\temp');" 'DOES NOT WORK
'sSQL = "insert into [" & wsNew.Name & "$] values ('25-Dec-2015','C:\temp')" 'works
Stop
oConn.Execute sSQL
Debug.Assert oConn.Errors.Count = 0
Stop
End Sub
On gets an error message of "Syntax error in INSERT INTO statement."
Ah.
It seems one adds square brackets around the column names
Dim sSQL As String
sSQL = "insert into [" & wsNew.Name & "$] ([TimeStamp],[Path]) VALUES ('31-Dec-2015','C:\temp');"

Rearranging columns in VBA

The current codes I am working on requires me to rearrange the columns in VBA. It has to arranged according to the header, and the headers are "V-d(1)", "V-g(1)", "I-d(1)", "I-g(1)", and this set repeats for numbers 2, 3, etc etc. (e.g V-d(2), I-g(4)). These data are usually jumbled up and I have to arrange them in ascending numbers.
It does not matter if V-g, V-d, I-d or I-g comes first.
Dim num, numadj As Integer
Dim colu, coladj
Range("A1").Select
Do While Range("A1").Offset(0, i - 1).Value <> ""
colu = ActiveCell.Value
coladj = ActiveCell.Offset(0, 1).Value
num = Left(Right(colu.Text, 2), 1)
numadj = Left(Right(coladj.Text, 2), 1)
If num > numadj Then
colu.EntireColumn.Cut Destination:=Columns("Z:Z")
coladj.EntireColumn.Cut Destination:=colu
Columns("Z:Z").Select.Cut Destination:=coladj
i = i + 1
Else
i = i + 1
End If
Loop
I am very new to VBA so please forgive me for any dumb codes that I have created!!! Thank you in advance everyone!
Consider an SQL and RegEx solution to select columns in a specified arrangement. SQL works in Excel for PC which can access Windows' Jet/ACE SQL Engine to query its own workbook like a database table.
Due to the variable nature of sets ranging 3-10, consider finding the highest number set by extracting the numbers from column headers with RegEx using the defined function, FindHighestNumberSet. Then have RunSQL subroutine call the function to build SQL string dynamically.
Below assumes you have data currently in a tab named DATA with an empty tab named RESULTS which will output query results. Two ADO connection strings are available.
Function (iterating across column headers to extract highest number)
Function FindHighestNumberSet() As Integer
Dim lastcol As Integer, i As Integer
Dim num As Integer: num = 0
Dim regEx As Object
' CONFIGURE REGEX OBJECT
Set regEx = CreateObject("VBScript.RegExp")
With regEx
.Global = True
.MultiLine = True
.IgnoreCase = False
.Pattern = "[^0-9]"
End With
With Worksheets("DATA")
lastcol = .Cells(7, .Columns.Count).End(xlToLeft).Column
For i = 1 To lastcol
' EXTRACT NUMBERS FROM COLUMN HEADERS
num = Application.WorksheetFunction.Max(num, CInt(regEx.Replace(.Cells(1, i), "")))
Next i
End With
FindHighestNumberSet = num
End Function
Macro (main module looping through result of above function)
Sub RunSQL()
On Error GoTo ErrHandle
Dim conn As Object, rst As Object
Dim strConnection As String, strSQL As String
Dim i As Integer
Set conn = CreateObject("ADODB.Connection")
Set rst = CreateObject("ADODB.Recordset")
' DRIVER AND PROVIDER CONNECTION STRINGS
' strConnection = "DRIVER={Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)};" _
' & "DBQ=" & Activeworkbook.FullName & ";"
strConnection = "Provider=Microsoft.ACE.OLEDB.12.0;" _
& "Data Source='" & ActiveWorkbook.FullName & "';" _
& "Extended Properties=""Excel 8.0;HDR=YES;"";"
' FIRST THREE SETS
strSQL = " SELECT t.[V-d(1)], t.[I-d(1)], t.[I-g(1)]," _
& " t.[V-d(2)], t.[I-d(2)], t.[I-g(2)]," _
& " t.[V-d(3)], t.[I-d(3)], t.[I-g(3)]"
' VARIABLE 4+ SETS
For i = 4 To FindHighestNumberSet
strSQL = strSQL & ", t.[V-d(" & i & ")], t.[I-d(" & i & ")], t.[I-g(" & i & ")]"
Next i
' FROM CLAUSE
strSQL = strSQL & " FROM [DATA$] t"
' OPEN DB CONNECTION
conn.Open strConnection
rst.Open strSQL, conn
' COLUMN HEADERS
For i = 1 To rst.Fields.Count
Worksheets("RESULTS").Cells(1, i) = rst.Fields(i - 1).Name
Next i
' DATA ROWS
Worksheets("RESULTS").Range("A2").CopyFromRecordset rst
rst.Close: conn.Close
Set rst = Nothing: Set conn = Nothing
MsgBox "Successfully ran SQL query!", vbInformation
Exit Sub
ErrHandle:
Set rst = Nothing: Set conn = Nothing
MsgBox Err.Number & " = " & Err.Description, vbCritical
Exit Sub
End Sub
You can sort vertically by a helper row with something like this (tested):
Sub test() ': Cells.Delete: [b2:d8] = Split("V-d(10) V-d(2) V-d(1)") ' used for testing
Dim r As Range: Set r = ThisWorkbook.Worksheets("Sheet1").UsedRange ' specify the range to be sorted here
r.Rows(2).Insert xlShiftDown ' insert helper row to sort by. (used 2nd row instead 1st so that it is auto included in the range)
r.Rows(2).FormulaR1C1 = "=-RIGHT(R[-1]C,LEN(R[-1]C)-3)" ' to get the numbers from the column header cells above, so adjust if needed
r.Sort r.Rows(2) ' sort vertically by the helper row
r.Rows(2).Delete xlShiftUp ' delete the temp row
End Sub

Excel UDF to return array from ACE SQL recordset

I am trying to use ACE sql to return array from recordset. The function works well with table ranges.
Problem: the function returns the right number of records if the query returns 2 or more records. However if only one record is found, all rows are filled repetitively with this one row. This is wrong but I cannot find the reason why.
In addition, I wish my function would return column names from Recordset. I have no idea how to glue it together with the array returned from recordset.
Here is the code, credits are due to the author of another solution that I am trying to adapt to my needs: Performing SQL queries on an Excel Table within a Workbook with VBA Macro
Function SQL(dataRange As Range, CritA As String) As Variant
Application.Volatile
Dim cn As ADODB.Connection
Dim rs As ADODB.Recordset
Dim currAddress As String
currAddress = ActiveSheet.Name & "$" & dataRange.Address(False, False)
strFile = ThisWorkbook.FullName
strCon = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & strFile _
& ";Extended Properties=""Excel 12.0;HDR=Yes;IMEX=1"";"
Set cn = CreateObject("ADODB.Connection")
Set rs = CreateObject("ADODB.Recordset")
cn.Open strCon
strSQL = "SELECT * FROM [" & currAddress & "]" & _
"WHERE [A] = '" & CritA & "' " & _
"ORDER BY 1 ASC"
rs.Open strSQL, cn
'SQL = rs.GetString
SQL = Application.Transpose(rs.GetRows)
Set rs = Nothing
Set cn = Nothing
End Function
If you want the field names, you will have to loop through the array from the recordset too:
With rs
If Not .EOF Then
vData = .getrows()
ReDim vDataOut(LBound(vData, 2) To UBound(vData, 2) + 1, LBound(vData, 1) To UBound(vData, 1))
' get headers
For i = 1 To .Fields.Count
vDataOut(0, i - 1) = .Fields(i - 1).Name
Next i
' Copy data
For x = LBound(vData, 2) To UBound(vData, 2)
For y = LBound(vData, 1) To UBound(vData, 1)
vDataOut(x + 1, y) = vData(y, x)
Next y
Next x
End If
End With
for example.

Exporting each Excel column to individual text or csv files?

I have an Excel worksheet with around 100 cols. Does anyone know of an easy way to write the contents of each column to a csv or txt file?
I don't have Excel in front of me, but I think this code is approximately what you need, give or take some syntax errors. It should write each column into a separate file, with each cell on a different row. It will work for arbitrary column heights, though the number of columns is in a variable (for now).
dim fso as FileSystemObject
dim ts as TextStream
dim i as Integer
dim myCell as Range
set fso = FileSystemObject
for i = 0 to TotalColumnNumber
' last argument, True, says to create the text file if it doesnt exist, which is
' good for us in this case
Set ts = fso.OpenTextFile("column_" & i, ForWriting, True)
' set mycell to the first cell in the ith column
set myCell = SheetName.cells(1,i)
' continue looping down the column until you reach a blank cell
' writing each cell value as you go
do until mycell.value = ""
ts.writeline mycell.value
set myCell = myCell.offset(1,0)
loop
ts.close
next
set ts = nothing
set fso = nothing
Let me know if that helps or not, I can take another look later if you would like
Perhaps
Dim cn As Object
Dim rs As Object
Dim strFile As String
Dim strCon As String
Dim strSQL As String
Dim i As Integer
''This is not the best way to refer to the workbook
''you want, but it is very conveient for notes
''It is probably best to use the name of the workbook.
strFile = ActiveWorkbook.FullName
''Note that if HDR=No, F1,F2 etc are used for column names,
''if HDR=Yes, the names in the first row of the range
''can be used.
''This is the Jet 4 connection string, you can get more
''here : http://www.connectionstrings.com/excel
strCon = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & strFile _
& ";Extended Properties=""Excel 8.0;HDR=Yes;IMEX=1"";"
''Late binding, so no reference is needed
Set cn = CreateObject("ADODB.Connection")
Set rs = CreateObject("ADODB.Recordset")
cn.Open strCon
''WHERE 1=1 = headers only, note hdr=yes above
strSQL = "SELECT * " _
& "FROM [Sheet1$] " _
& "WHERE 1=1"
''Open the recordset for more processing
''Cursor Type: 3, adOpenStatic
''Lock Type: 3, adLockOptimistic
''Not everything can be done with every cirsor type and
''lock type. See http://www.w3schools.com/ado/met_rs_open.asp
rs.Open strSQL, cn, 3, 3
''Output including nulls. Note that this will fail if the file
''exists.
For i = 0 To rs.Fields.Count - 1
strSQL = "SELECT [" & rs.Fields(i).Name & "] " _
& "INTO [Text;HDR=YES;FMT=Delimited;IMEX=2;DATABASE=C:\Docs\]." _
& rs.Fields(i).Name & ".CSV " _
& "FROM [Sheet1$] "
''To skip nulls and empty cells, add a WHERE statement
''& "WHERE Trim([" & rs.Fields(i).Name & "] & '')<>'' "
cn.Execute strSQL
Next
''Tidy up
rs.Close
Set rs = Nothing
cn.Close
Set cn = Nothing
A very quick line to get you started ...
for i = 1 to 100
open "file" & i & ".txt" as #1
for each c in columns(i).cells
print #1, c.value
next c
close #1
next i

Resources