Excel - Using COUNTIF/COUNTIFS across multiple sheets/same column - excel

I am trying to "COUNT" the number of a certain object in column I (in this instance) across multiple sheets. That value in column I is the result of a formula (if it matters). So far I have:
=COUNTIF('Page M904'!I:I,A13)+COUNTIF('Page M905'!I:I,A13)+COUNTIF('Page M906'!I:I,A13)
which works, but I am going to have 20 something pages to scan through. I would like to avoid having a page long formula.
I have tried
=COUNTIFS('Page M904:Page M906'!I:I,A13) and
=COUNTIF('Page M904:Page M906'!I:I,A13)
but that results in a #VALUE.
And I think
=COUNTIFS('Page M904'!I:I,A14,'Page M905'!I:I,A14,'Page M906'!I:I,A14)
is a misapplication of the COUNTIFS because I get 0 when it should be 35.
I am trying to avoid using VBA for this application. But if has to be, then it has to be :) Thanks in advance for your time and help.

This could be solved without VBA by the following technique.
In this example I am counting all the threes (3) in the range A:A of the sheets Page M904, Page M905 and Page M906.
List all the sheet names in a single continuous range like in the following example. Here listed in the range D3:D5.
Then by having the lookup value in cell B2, the result can be found in cell B4 by using the following formula:
=SUMPRODUCT(COUNTIF(INDIRECT("'"&D3:D5&"'!A:A"), B2))

I am trying to avoid using VBA. But if has to be, then it has to be:)
There is quite simple UDF for you:
Function myCountIf(rng As Range, criteria) As Long
Dim ws As Worksheet
For Each ws In ThisWorkbook.Worksheets
myCountIf = myCountIf + WorksheetFunction.CountIf(ws.Range(rng.Address), criteria)
Next ws
End Function
and call it like this: =myCountIf(I:I,A13)
P.S. if you'd like to exclude some sheets, you can add If statement:
Function myCountIf(rng As Range, criteria) As Long
Dim ws As Worksheet
For Each ws In ThisWorkbook.Worksheets
If ws.name <> "Sheet1" And ws.name <> "Sheet2" Then
myCountIf = myCountIf + WorksheetFunction.CountIf(ws.Range(rng.Address), criteria)
End If
Next ws
End Function
UPD:
I have four "reference" sheets that I need to exclude from being scanned/searched. They are currently the last four in the workbook
Function myCountIf(rng As Range, criteria) As Long
Dim i As Integer
For i = 1 To ThisWorkbook.Worksheets.Count - 4
myCountIf = myCountIf + WorksheetFunction.CountIf(ThisWorkbook.Worksheets(i).Range(rng.Address), criteria)
Next i
End Function

My first post...
UDF I managed quickly to compile.
Usage:
Select 3D range as normal and enclose is into quotation marks like below...
=CountIf3D("'StartSheet:EndSheet'!G16:G878";"Criteria")
Advisably sheets to be adjacent to avoid unanticipated results.
Public Function CountIf3D(SheetstoCount As String, CriteriaToUse As Variant)
Dim sStarSheet As String, sEndSheet As String, sAddress As String
Dim lColonPos As Long, lExclaPos As Long, cnt As Long
lColonPos = InStr(SheetstoCount, ":") 'Finding ':' separating sheets
lExclaPos = InStr(SheetstoCount, "!") 'Finding '!' separating address from the sheets
sStarSheet = Mid(SheetstoCount, 2, lColonPos - 2) 'Getting first sheet's name
sEndSheet = Mid(SheetstoCount, lColonPos + 1, lExclaPos - lColonPos - 2) 'Getting last sheet's name
sAddress = Mid(SheetstoCount, lExclaPos + 1, Len(SheetstoCount) - lExclaPos) 'Getting address
cnt = 0
For i = Sheets(sStarSheet).Index To Sheets(sEndSheet).Index
cnt = cnt + Application.CountIf(Sheets(i).Range(sAddress), CriteriaToUse)
Next
CountIf3D = cnt
End Function

I was looking to do the same thing, and I have a work around that seems to be less complicated using the Frequency and Index functions. I use this part of the function from averaging over multiple sheets while excluding the all the 0's.
=(FREQUENCY(Start:End!B1,-0.000001)+INDEX(FREQUENCY(Start:End!B1,0),2))

Related

Return an Array of Only Numbers From a Range With Mixed Datatypes

Related to the following screenshot, the formula
=IF(ISNUMBER($A$1:$A$5),$A$1:$A$5)
will evaluate to the following array
{1;FALSE;2;44644;3}
but I only need it to return the numbers
{1;2;3}
How can this be achieved (getting rid of the dates and booleans)?
Note that ISNUMBER has already gotten rid of error values and whatnot.
Utilization in VBA
Instead of the loop and whatnot in the first procedure I want to simplify by evaluating the correct formula in the second procedure.
Correct
Sub CorrectVBA()
' Result:
' 1
' 2
' 3
Const rgAddress As String = "A1:A5"
Dim ws As Worksheet: Set ws = ThisWorkbook.Worksheets("Sheet1")
Dim rg As Range: Set rg = ws.Range(rgAddress)
Dim Data As Variant: Data = rg.Value
Dim Arr() As Double
Dim Item As Variant
Dim r As Long
Dim n As Long
For r = 1 To UBound(Data, 1)
Item = Data(r, 1)
If WorksheetFunction.IsNumber(Item) Then
If Not IsDate(Item) Then
n = n + 1
ReDim Preserve Arr(1 To n)
Arr(n) = Item
End If
End If
Next r
If n = 0 Then Exit Sub
For n = 1 To UBound(Arr)
Debug.Print Arr(n)
Next n
End Sub
Wrong
Something like this is what I want to do in this particular case.
It is wrong because the formula is wrong.
Sub WrongVBA()
' Result:
' 1
' False
' 2
' 44644
' 3
Const rgAddress As String = "A1:A5"
Dim ws As Worksheet: Set ws = ThisWorkbook.Worksheets("Sheet1")
Dim Data As Variant
Data = ws.Evaluate("IF(ISNUMBER(" & rgAddress & ")," & rgAddress & ")")
Dim r As Long
For r = 1 To UBound(Data, 1)
Debug.Print Data(r, 1)
Next r
End Sub
Final Word
Thank you all.
I have decided to accept objectively JvdV's answer since, if not already, Office 365 is becoming a standard and its new functions make things so much easier.
Personally, the most useful answers were those of T.M. and Ron Rosenfeld.
Also, thanks to Can.U and Domenic.
This is not easy through formulae because Excel can not distinguish between say 44644 being an integer or meant to be a date unless you look at formatting of the cel. The only reasonable way of doing this that I can think of is to use CELL(), as per this older post here on SO. It can't return an array correctly on its own so I came up with the following using BYROW():
Formula in C1:
=FILTER(A1:A5,BYROW(SEQUENCE(5,,0),LAMBDA(a,CELL("format",OFFSET(A1,a,0))="G"))*ISNUMBER(A1:A5))
This would work to filter out the dates (given the format in your data) since the cells that are not formatted as date would return "G" ('General', see the link to the ms-documentation). My limited testing suggested that this would work to filter out dates from the equation as per the question.
Note: To be more specific, you can exclude any cells that have this specific formatting ('mm/dd/yyyy') you have shown, from the equation through:
=FILTER(A1:A5,BYROW(SEQUENCE(5,,0),LAMBDA(a,LEFT(CELL("format",OFFSET(A1,a,0)))<>"D"))*ISNUMBER(A1:A5))
EDIT: The above solution would be ms365 exclusive. For Excel 2019, one way to do this is:
=FILTERXML("<t><s>"&TEXTJOIN("</s><s>",,IF(ISNUMBER(A1:A5),A1:A5,""))&"</s></t>","//s")
It's an CSE-entered formula. If done correctly the returned array would be {1;2;44644;3};
We could also use =FILTERXML("<t><s>"&TEXTJOIN("</s><s>",,A1:A5)&"</s></t>","//s[.*0=0]") however, since TEXTJOIN() has a limit I thought it would be wise to proces any non-number into an empty string beforehand;
There is no way (I can think of) to exclude the dates in this version of Excel.
365
=FILTER(A1:A5,ISNUMBER(-TEXT("1/1/"&A1:A5,"e/m/d")))
or older version:
=N(OFFSET(A1,SMALL(IF(ISNUMBER(-TEXT("1/1/"&A1:A5,"e/m/d")),ROW(1:5)-1),ROW(INDIRECT("1:"&COUNT(-TEXT("1/1/"&A1:A5,"e/m/d"))))),))
Alternative via XML Spreadsheet udf
Though the question shows the excel-formula tag, you might consider to use a user-defined function analyzing the column's Value(xlRangeValueXMLSpreadsheet) or simply Range.Value(11) xml content equivalent.
This approach benefits from the fact that the relevant data types of your question are explicitly distinguished between "Number" and "DateTime" formats.
As the resulting xml content includes namespace definitions like "ss:" I preferred to use late bound MsXml2 for node extraction. - Afaik namespacing can't be applied in FilterXML (a possible work-around would be reduce the whole content via split and replace any namespaces to allow this, too).
The following example udf assumes a 1-column range input (without further error handling) and gets all "real" numbers via a node select Set cells = xDoc.SelectNodes("//ss:Cell[ss:Data/#ss:Type='Number']").
Function Nums(rng As Range)
'[0]Get Value(11)
Dim s As String
s = rng.value(xlRangeValueXMLSpreadsheet) ' or: rng.Value(11)
'[1]Set xml document to memory
Dim xDoc As Object: Set xDoc = CreateObject("MSXML2.DOMDocument.6.0")
'[2]Add namespaces
xDoc.SetProperty "SelectionNamespaces", _
"xmlns:ss='urn:schemas-microsoft-com:office:spreadsheet' " & _
"xmlns:ht='http://www.w3.org/TR/REC-html40'"
'[3]Get cells with Data/#Type "Number"
If xDoc.LoadXML(s) Then ' load wellformed string content
Dim cell As Object, cells As Object
Set cells = xDoc.SelectNodes("//ss:Cell[ss:Data/#ss:Type='Number']") ' XPath using namespace prefixes
Dim tmp(): ReDim tmp(1 To cells.Length, 1 To 1)
For Each cell In cells
Dim i as long: i = i + 1
tmp(i, 1) = cell.Text
Next cell
'[4]return 1-column array
Nums = tmp
End If
End Function
Example call
In tabular Excel e.g. Nums(A1:A5) (resulting in a spill range if you dispose of dynamic array feature, otherwise via CSE), or
via VBA e.g.
Dim rng As Range
Set rng = Sheet1.Range("A1:A5")
Dim results
results = Nums(rng)
rng.Offset(, 1).Resize(UBound(results), 1) = results
As I mentioned in my comments, I don't know of a reliable method of determining if an entry is a date type, since dates in Excel are stored as numbers.
Since you have also written that excluding the dates is not an absolute requirement, here are several formulas that will work depending on your version of Excel.
O365
C1: =FILTER(A1:A5,ISNUMBER(A1:A5))
results will spill down
Earlier versions of Excel
E1: =IFERROR(INDEX($A$1:$A$5,AGGREGATE(15,6,1/ISNUMBER($A$1:$A$5)*ROW(A1:$A$5),ROW(INDEX($A:$A,1):INDEX($A:$A,SUM(--ISNUMBER($A$1:$A$5)))))),"")
entered as an array formula (with ctrl + shift + enter over a range large enough to include all the results)
G1: =IFERROR(INDEX($A$1:$A$5,AGGREGATE(15,6,1/ISNUMBER($A$1:$A$5)*ROW($A$1:$A$5),ROWS($A$1:A1))),"")
entered as a normal formula, then fill down until you start returning blanks

VBA Function to find Activecell Table Row

As a learning exercise & possible use in future code I have created my first Excel VBA function to return the activecell row number in any Excel Table (as opposed to the sheet itself) . Essentially it simply finds the active row in the sheet, then finds the row number of the table header which is then subtracted from the cell row number to return the row number of the table which can then be used in subsequent code. However, while it works, it dosen't look the most efficient Can anyone improve it?
Sub TableRow()
Dim LORow As Integer
Dim TbleCell As Range
Set TbleCell = Activecell
Call FuncTableRow(TbleCell, LORow)
MsgBox LORow
End Sub
Public Function FuncTableRow(ByRef TbleCell As Range, LORow As Integer) As Range
Dim LOName As String
Dim LOHeaderRow, Row As Integer
LOName = Activecell.ListObject.Name
Row = Activecell.Row
LOHeaderRow = ActiveSheet.ListObjects(LOName).HeaderRowRange.Row
LORow = Row - LOHeaderRow
Debug.Print (LORow)
End Function
This question will probably get closed for not being specific enough but the most obvious item (to me) is your usage of a custom function. Your function is not actually returning anything, it's only running a debug print. To have your function actually return the row number, you would set it as a type Long (not integer) and include the function name = to the number.
I didn't actually test your function but assuming LORow is dubug printing the proper answer then it should work like this:
Public Function FuncTableRow(ByRef TbleCell As Range, LORow As Integer) As Long
Dim LOName As String
Dim LOHeaderRow, Row As Integer
LOName = Activecell.ListObject.Name
Row = Activecell.Row
LOHeaderRow = ActiveSheet.ListObjects(LOName).HeaderRowRange.Row
LORow = Row - LOHeaderRow
Debug.Print (LORow)
FuncTableRow = LORow
End Function
You also don't Call a function, you can just insert it as itself in a subroutine.
You are using LORow as an input variable but then changing it. That's typically a bad practice.
You should not be using ActiveSheet grab the worksheet from TbleCell.Worksheet
You would almost never use activecell as part of a Custom Formula.
Dim LOHeaderRow, Row As Integer should actually be Dim LOHeaderRow as Long, Row As Long. As you currently have it LOHeaderRow is undefined/Variant.
There's probably more. I would restart your process with a simpler task of returning the last used cell in a worksheet. There's a dozen ways to do this and lots of help examples.
Take a look at this TheSpreadsheetGuru.
Here are some variables that might help you.
Sub TableVariables()
Dim ol As ListObject: Set ol = ActiveSheet.ListObjects(1)
Dim olRng As Range: Set olRng = ol.Range ' table absolute address
Dim olRngStr As String: olRngStr = ol.Range.Address(False, False) ' table address without absolute reference '$'
Dim olRow As Integer: olRow = ol.Range.Row ' first row position
Dim olCol As Integer: olCol = ol.Range.Column ' first column position
Dim olRows As Long: olRows = ol.Range.Rows.Count ' table rows including header
Dim olCols As Long: olCols = ol.ListColumns.Count ' table columns
Dim olListRows As Long: olListRows = ol.ListRows.Count ' table rows without header
End Sub

Find maximum value in a range of strings

I have a spreadsheet with number of different sheets. Each sheet has a column with unique ID which is made up as follows: AD-S001, AD-S002, AD-S003 etc. The next sheet's unique ID could be AD-M001, AD-M002 etc.
I am pretty new to VBA in excel and am trying to write some code to create a new record, incrementing the unique ID. The unique ID's are sometimes not sorted so I need to find the largest in the range and then increment it by 1.
I have the other code already, just trying to add the bit that finds the largest value in the range of strings and increments it by 1.
Grateful if someone could assist me in how best to write this sub-routine.
Thanks
Use the next function, please:
Function newID(sh As Worksheet) As String
Dim lastR As Long, strID As String, arr As Variant, i As Long
lastR = sh.Range("A" & Rows.count).End(xlUp).Row
arr = sh.Range("A2:A" & lastR).Value
strID = left(sh.Range("A2").Value, 4)
For i = 1 To UBound(arr)
arr(i, 1) = CLng(Right(arr(i, 1), 3))
Next i
newID = strID & Format(WorksheetFunction.Max(arr) + 1, "000")
End Function
It can be called/used in this way:
Sub testNewID()
Dim sh As Worksheet
Set sh = ActiveSheet 'use here the necessary worksheet
MsgBox newID(sh)
End Sub
A non-VBA alternative, see if it is acceptable.
Set the format for the column as below
"AD-S"000
And then you can simply use a MAX formula to get the maximum value in preceding cells. Assuming first numbered cell is A2, formula in cell A3 would be:
=MAX($A$2:A2)+1
which can then be copied down as much needed.

vba code that is in several cells and checks several ranges

Can anyone help
I have this formula in several cells, checking several ranges!
Is there a way for me to convert this to VBA so that i only have to select one range in each cell?
Thanks for the help.
=IF(COUNTIF(BE95:BE99;"Falhou")>0;"Falhou";IF(COUNTIF(BE95:BE99;"Falhou Condicionamente")>0;"Falhou Condicionamente";IF(COUNTIF(BE95:BE99;"Passou Condicionamente")>0;"Passou Condicionamente";IF(COUNTIF(BE95:BE99;"Passou")>0;"Passou"))))
Perhaps this is what you want, for it to work let's insert a module and put this code into this module.
Then just type following formulas:
= Test (BE95: BE99)
Function test(rng As Range)
Dim str As Variant
For Each str In Array("Falhou", "Falhou Condicionamente", "Passou Condicionamente", "Passou")
If Not IsError(Application.Match(str, rng, 0)) Then test = str:Exit for 'if match exist then return value
Next str
End Function
The function below has one feature your worksheet function doesn't have: It returns "Falhou" if the range contains none of the 4 count criteria.
Function Passou(Rng As Range) As String
' '=IF(COUNTIF(BE95:BE99;"Falhou")>0;"Falhou";
' IF(COUNTIF(BE95:BE99;"Falhou Condicionamente")>0;"Falhou Condicionamente";
' IF(COUNTIF(BE95:BE99;"Passou Condicionamente")>0;"Passou Condicionamente";
' IF(COUNTIF(BE95:BE99;"Passou")>0;"Passou"))))
Dim Sp() As String
Dim i As Integer
Sp = Split("Falhou,Falhou Condicionamente,Passou Condicionamente,Passou", ",")
For i = UBound(Sp) To 1 Step -1
If Application.CountIf(Rng, Sp(i)) Then Exit For
Next i
Passou = Sp(i)
End Function
Call the UDF from the worksheet specifying the range to be searched.
= Passou($BE$95:$BE$99)
Absolute addressing of the range enables copying of the formula across rows and columns.

Stumped with WorksheetFunction.Match() in Excel

This is some VBA code I've written for Excel. I'm trying to match entries in Sheet1 with those in Sheet2. The structure of both sheets is as follows:
DATE | ID |
----- ----
Date1 ID1
Date2 ID2...
In my code, I loop through the rows of the first sheet, and set the values from each particular row as part of my MATCH() query, in hopes of finding these same values in the second sheet. When I do, I want MATCH() to return the row index it finds these values in, so I can use that same row to input further information from the first sheet. This query uses multiple criteria, as indicated by both the value and searchRange variables (I'm trying to use the multiple criteria via concatenation method, as seen in this article).
The problem is, I consistently get a WorksheetFunction.Match could not be used error. When I used one single criteria (the ID), the function worked. When I tried to use multiple ones, it failed, even though I followed the instructions seen in the previously linked article. Any suggestions or ideas to fix this would be appreciated.
Sub runComparison(Sheet1 As String, Sheet2 As String)
Dim rowCount As Variant, columnCount As Variant, information As Variant
Dim counter As Integer
Dim value As String, searchRange As String
Sheets(Sheet2).Select
'Array of the number of rows in both sheets
rowCount = Array(Sheets(Sheet1).Cells(Rows.count, "A").End(xlUp).row, Sheets(Sheet2).Cells(Rows.count, "A").End(xlUp).row)
'Array of the number of columns in both sheets
columnCount = Array(Sheets(Sheet1).Cells(1, Columns.count).End(xlToLeft).Column, Sheets(Sheet2).Cells(1, Columns.count).End(xlToLeft).Column)
'The range in which we will look for the date and the ID
searchRange = CStr(Range(Cells(2, 1), Cells(rowCount(1), 1)).Address & "&" & Range(Cells(2, 2), Cells(rowCount(1), 2)).Address)
counter = 2
Do Until counter = rowCount(0)
'Sets the search term equal to the current cell in Sheet1
value = Sheets(Sheet1).Cells(counter, 1) & "&" & Sheets(Sheet2).Cells(counter, 2)
' Attempts to set the cell in the 8th column in the same row in which the search term is found equal to a certain value from the search term's row
Cells(WorksheetFunction.Match(value, searchRange, 0), 8) = Sheets(Sheet1).Cells(counter, columnCount(0)).value
counter = counter + 1
Loop
End Sub
Edit: Here's some sample input
value = '7/14/2014&ESTUOUW1046465464'
searchRange = '$A2:$A298&$B2:B298'
UPDATED
Thanks for clarifying in comments. I removed my original answer as it pertains only to the regular "Match" function, and I see the reference/example and understand what you're trying to do now which involves an array formula.
Let's try this using Application.Evaluate which will avoid the need to put this formula in a cell. Using the example data from MS, I did this which seems to work:
Sub test()
Dim value As String
Dim srcRange As String
value = "D2&E2"
srchRange = "$A$2:$A$5&$B$2:$B$5"
Debug.Print Application.Evaluate("=MATCH(" & value & "," & srchRange & ",0)")
End Sub
Applying that in your code, I think would be like below. YOu will still want to Dim matchVal as Variant to hold the result of the formula evaluation, I think. Then do this:
Do Until counter = rowCount(0)
'Sets the search term equal to the current cell in Sheet1
value = Sheets(Sheet1).Cells(counter, 1) & "&" & Sheets(Sheet2).Cells(counter, 2)
'## Assign the result of the Match function to a variable
matchVal = Application.Evaluate("=MATCH(" & value & "," & searchRange & ",0)")
'## Check for errors, and handle as needed:
If IsError(matchVal) Then
'modify as needed, this highlight the cell with the non-matched value
' you might omit this line and simply ignore it, or you could
' display a MsgBox prompt, etc.
Sheets(Sheet1).Cells(counter, columnCount(0)).Interior.ColorIndex = 6
Else:
Cells(matchVal, 8) = Sheets(Sheet1).Cells(counter, columnCount(0)).value
End If
counter = counter + 1
Loop

Resources