Excel SpecialCells does not work when used inside spreadsheet - excel

I wrote a VBA function in an attempt to get around using Array Formulas to check a range of cells against a condition and return some column offset.
It's basically a Sumif that, instead of returning a sum, returns a range of cells that would have been summed up.
The problem I'm encountering is that the code runs differently when being called inside a worksheet versus another function, specifically the .SpecialCells does not limit the range when needed.
For example, in the code I perform the comparison on only cells that have formulas or constants, and this works fine to limit the calculations performed when calling from another macro or the immediate window, but if I enter it as a formula in a sheet, it does not limit it at all (if I specified the entire column as a comparison, even if most of the cells in the column are blank it still goes through all 1048576 cells)
The important code is as follows:
For Each CheckCell In Check.SpecialCells(xlCellTypeConstants)
For Check as Range("A:A"), This ideally would run for, say 132 cells that have a constant in it, but instead runs the entire column.
Any ideas how to get this to work more sensibly? The rest of the code works very well for what I need, I just don't want to have it spending several seconds calculating the entire column for every cell this formula is used in.
Complete function:
Function RangeIf(returnColumn As Range, Check As Range, Condition As String) As Range
'Exit Function
Dim Operator As Integer, HasOperator As Boolean, TheColumn As String, CheckCell As Range, Passed As Boolean, ReturnRange As Range
HasOperator = True
Operator = 0
TheColumn = Mid(returnColumn.Cells(1, 1).Address, 2)
TheColumn = "$" & Mid(TheColumn, 1, InStr(1, TheColumn, "$"))
While HasOperator
Select Case Mid(Condition, 1, 1)
Case "<"
Operator = Operator Or 1
Condition = Mid(Condition, 2)
Case ">"
Operator = Operator Or 2
Condition = Mid(Condition, 2)
Case "="
Operator = Operator Or 4
Condition = Mid(Condition, 2)
Case Else
HasOperator = False
End Select
Wend
For Each CheckCell In Intersect(Check, Check.Parent.UsedRange).Cells
Passed = False
'UpdateStatusBar "Processing Cell: " & CheckCell.Address
Select Case Operator
Case 0, 4 'No op or Equals
If CheckCell.Value = Condition Then Passed = True
Case 1 ' Less than
If CheckCell.Value < Condition Then Passed = True
Case 2 ' Greater than
If CheckCell.Value > Condition Then Passed = True
Case 3 ' Not
If CheckCell.Value <> Condition Then Passed = True
Case 5 ' Less or Equal
If CheckCell.Value <= Condition Then Passed = True
Case 6 ' Greater or Equal
If CheckCell.Value >= Condition Then Passed = True
End Select
If Passed Then
If Not ReturnRange Is Nothing Then
Set ReturnRange = Union(ReturnRange, Range(TheColumn & CheckCell.Row))
Else
Set ReturnRange = Range(TheColumn & CheckCell.Row)
End If
End If
Next CheckCell
Set RangeIf = ReturnRange
End Function

SpecialCells does not work in UDFs. It's a limitation of Excel. Here's a list of things that don't work in UDFs.
http://www.decisionmodels.com/calcsecretsj.htm
You have to loop through the cells individually. Start my limiting Check to only the UsedRange.
For Each CheckCell in Intersect(Check, Check.Parent.UsedRange).Cells
That will keep it under a million, probably. You can reduce it further, but it will be specific to your situation.

Related

RIGHT function doesn't work when put within an IF statement

I want to create an IF condition using the RIGHT function. It would look up the 4 last digits within a cell and compare it to another cell, then perform actions if it's a match.
Here's a simplified version of the code I toyed with. The action to be performed in this experience is just to display the counter in a cell.
Public vCounter
Sub Counter()
vCounter = 0
Sheets.Add.Name = "Test"
'The cells the RIGHT function will operate from (A1, A2 and A3)
Sheets("Test").Range("A1") = "123"
Sheets("Test").Range("A2") = "456"
Sheets("Test").Range("A3") = "789"
'The cells the result of the RIGHT function will be compared to (B1, B2 and B3)
Sheets("Test").Range("B1") = "23"
Sheets("Test").Range("B2") = "456"
Sheets("Test").Range("B3") = "89"
'This cell (G3) shows the result of a RIGHT function, considering the
'last two digits in A1, as an experience; it works.
Sheets("Test").Range("G3") = Right(Sheets("Test").Cells(1, 1), 2)
For i = 1 To 3
'The RIGHT function considers the two last digits of, successively,
'A1, A2 and A3, and those are compared to, respectively,
'B1, B2 and B3. For some reason, it doesn't work here.
If Right(Sheets("Test").Cells(i, 1), 2) = Sheets("Test").Cells(i, 2) Then
vCounter = vCounter + 1
End If
Next i
'This cell (E3) shows the counter, to test whether or not the If
'condition with the RIGHT function works. By changing the contents
'of the cells I compare between each other, I can check whether or
'not it counts correctly.
Sheets("Test").Range("E3") = vCounter
End Sub
Here's what I get:
The sheet that I get when I run this procedure
In conclusion, in this experience, the RIGHT function somehow doesn't work since the vCounter doesn't get to 2. It stays at 0, showing it doesn't count at all. I deduce from this result that the problem resides in the IF statement containing the RIGHT function. Maybe the For Loop has to do with it, but I doubt it.
Any thoughts?
Even though you're writing string values to your sheet, Excel will automatically assume them to be numeric values, so when you read them back you will be getting values of type Variant/Double.
If you pass one of those Doubles through Right() though, it will return a Variant\String, and it's that comparison between Variant\String and Variant\Double which seems to be failing.
Some test code:
Sub Tester()
Dim ws As Worksheet, v As Variant
Set ws = ThisWorkbook.Worksheets("Test")
ws.Range("A1").Value = "123"
ws.Range("B1").Value = "23"
'Comparing values...
Debug.Print Right(ws.Range("A1").Value, 2) = ws.Range("B1").Value '>> False (Variant\String vs Variant\Double)
Debug.Print Right(ws.Range("A1").Value, 2) = CStr(ws.Range("B1")) '>> True (Variant\String vs Variant\String)
End Sub

If range cells contains any numbers or blank cells, then

I am trying to create a VBA project like this, but I'm having a hard time using the LIKE function and nothing seems to happen when I hit the run button.
What I'm trying to do:
If the first digit is either a number or a blank cell in B4:B245, then enter a text in range D4:245.
If the last digit of the numeric is even in C4:C245, then enter a text in range D4:D245.
More info:
Product codes were imported
LEFT function was used to find the "First digit of product code"
RIGHT function was used to find the "Numeric digits of product code"
My current position in excel and VBA:
Sub number()
Dim first As Range
Set first = Range("B4:B259")
Dim numeric As Range
Set numeric = Range("C4:B259")
Dim DColumn As Range
Set DColumn = Range("D4:D259")
For Each first In DColumn
If first Like " " Then
DColumn = "Invalid Part Number"
DColumn.Interior.ColorIndex = 6
End If
Next
End Sub
The below macro will perform 3 tests & each will get it's own output.
Check for Numeric or blank first character
Check for Even ending character
Check for Odd ending character
These test are not in unison - the output will be one, or none. As soon as a test statement is TRUE, the loop will end for that cell and other values will not be tested.
For example, this macro will not provide you outputs when #1 & #2 from above are true. It will only tell you if #1 is true.
This code does not require you to split the product codes. The macro will work with them as is
Sub MyNum()
Dim xCell As Range, Product_Code As Range
Set Product_Code = Sheets("Sheet1").Range("A2:A9") '<-- Update sheet name
For Each xCell In Product_Code
If IsNumeric(Left(xCell, 1)) Or Left(xCell, 1) = " " Then
xCell.Offset(0, 1) = "Invalid Product: Char 1 = Numeric or Null"
ElseIf Right(xCell, Len(xCell) - 1) Mod 2 = 0 Then
xCell.Offset(0, 1) = "Even Ending Range"
ElseIf Right(xCell, Len(xCell) - 1) Mod 2 <> 0 Then
xCell.Offset(0, 1) = "Odd Ending Range"
End If
Next xCell
End Sub

If "iserror" FOR loop with varying object type

I have a column that has either string types (ex. "N/A") or numbers in it. I'm trying to write a vba code that if the FOR loop comes across a string type it'll be converted to a 0. Essentially what's happening is the code goes down a column (K) of values (ex. $10, $600, $5200, N/A, $5), and keeps the cell value if it's a number and if a cell has text in it then the text is converted to a 0. The range is (K6:K10), the formula would look something like,
=If(iserror(K6/10) = TRUE, 0, K6)
Code:
Dim a As Integer
Dim DilutedRev As Integer
For a = 6 To 10
DilutedRev = ActiveCell(a, 11).Value
If IsError(DilutedRev / 100) = True Then
ActiveCell(a, 11) = 0
Else
ActiveCell(i, 11) = DilutedRev
End If
Use the IsNumeric function to determine if the cell contains a number.
Dim a As Long
With ActiveSheet
For a = 6 To 10
If IsNumeric(.Cells(a, "K").Value2) Then
.Cells(a, 11) = CLng(.Cells(a, "K").Value2)
Else
.Cells(a, 11) = 0
End If
Next a
End With
Using the ActiveCell property like you did was not 'best practise' due to the relative origin point¹; better to use the Range.Cells property.
VBA functions that return boolean values (e.g. IsNumeric, IsError, etc) do not have to be compared to True or False. They are already either True or False.
I've used the Range.Value2 property to check but the Range.Value property could be used just as well.
It is usually worthwhile explicitly defining the Range.Parent worksheet property rather then relying implicitly on the ActiveSheet property.
¹ Strictly speaking, there is nothing wrong with offsetting the ActiveCell as you did but the result is completely relative to the current selection on the worksheet and is generally not considered 'best practise'. With D5 selected on the worksheet, activecell(1,1) references D5 and activecell(2,3) references F6. In a very special circumstance, this behavior may be desirable but generally speaking it is better to use the Range.Cells property and use the row_number and column_number to reference the cell from the Worksheet Object perspective, not as an offset position from the ActiveCell.
First as it was pointed out in your last question; Activecell refers to just that the activecell it is one cell and not a range. You want Cells().
Next by redeclaring the variable inside the loop it allows Excel to determine its type each time. If not it will be the type that excel assigns the first time and you cannot store a text value in an Integer type.
Then we test that variable if it is numeric or not.
But you could skip that and do what #Jeeped suggests.
Dim a As Integer
For a = 6 To 10
Dim DilutedRev
DilutedRev = Cells(a, 11).Value
If Not isnumeric(DilutedRev) Then
Cells(a, 11) = 0
Else
Cells(a, 11) = DilutedRev
End If

MIN/MAX on text sort order

In SQL Server, MIN and MAX can act on text that doesn't evaluate to numbers, returning the text item with the lowest or highest text sort order, or as it's known in SQL Server-speak, "collation order".
Is it possible to do that in Excel without going to a UDF that actually sorts?
For example, for MIN("bb","aa","cc") to return "aa", and MAX("bb","cc","aa") to return "cc".
Excel's MIN/MAX ignore text, and although MINA/MAXA can work on text, they break on text that doesn't resolve to a number. LARGE/SMALL don't do it either.
FWIW, a coworker asked me how to do this in a pivot. I don't see a way without going to a custom function. Am I wrong?
This array formula looks promising. since it is an array it needs to be entered with ctrl-shift-enter.
Max:
=INDEX(A2:A6,MATCH(0,COUNTIF(A2:A6,">"&A2:A6), 0))
Min:
=INDEX(A2:A6,MATCH(0,COUNTIF(A2:A6,"<"&A2:A6), 0))
Change the three ranges to what you want.
The max and min versions are the same except for the > versus <.
I believe you are correct, a custom function is best. The good thing to note is the normal comparator operators work similarly as you described.
Public Function MinStr(ByVal strVal As Range) As String
Dim i As Integer
Dim cell As Range
MinStr = ""
'Check to make sure the range is not empty
if strVal.Rows.Count > 0 then
'Initialize MinStr to a known value
MinStr = strVal.cells(1,1).Value
'Iterate through the entire range
For Each cell in strVal.Cells
if(MinStr > cell.Value) then
MinStr = cell.Value
end if
Next cell
end if
End Function
Public Function MaxStr(ByVal strVal As Range) As String
Dim i As Integer
Dim cell As Range
MaxStr = ""
'Check to make sure the range is not empty
if strVal.Rows.Count > 0 then
'Initialize MaxStr to a known value
MaxStr = strVal.cells(1,1).Value
'Iterate through the entire range
For Each cell in strVal.Cells
if(MaxStr < cell.Value) then
MaxStr = cell.Value
end if
Next cell
end if
End Function

Generate new Date series based on common dates from two date series

I am trying to compare two data series with dates and on a third column show ONLY the dates that are common in both data series (ordered in descending mode). A friend of mine helped me put together some code that seems to work but it seems to be taking a long time to generate the result when I have quite a long series of data. Is there a way to write this code differently that might get calculated faster? (I am currently using excel 2010.
The Function I enter on D2 and then I copy it down is: =next_duplicate(A2:$A$535,B2:$B$535,D1:$D$1)
Function next_duplicate(list1, list2, excluded)
For Each c In list1
If WorksheetFunction.CountIf(excluded, c) = 0 Then
If WorksheetFunction.CountIf(list2, c) > 0 Then
next_duplicate = c
Exit For
End If
End If
Next c
If next_duplicate = 0 Then
next_duplicate = "N/A"
End If
End Function
You can do this without VBA.
In Column C use COUNTIF to extract dates that appear only in both Columns A and B
=IF(COUNTIF($B$2:$B$7,"="&A2) > 0, A2, 0)
Then in Column D use an array formula (from here) to sort and remove blanks. Don't forget to select the range and then press control, shift and enter.
=INDEX(C2:C7, MATCH(LARGE(IF(ISBLANK(C2:C7), "", IF(ISNUMBER(C2:C7), COUNTIF(C2:C7, "<"&C2:C7), COUNTIF(C2:C7, "<"&C2:C7)+SUM(IF(ISNUMBER(C2:C7), 1, 0))+1)), ROW()-ROW($D$2)+1), IF(ISBLANK(C2:C7), "", IF(ISNUMBER(C2:C7), COUNTIF(C2:C7, "<"&C2:C7), COUNTIF(C2:C7, "<"&C2:C7)+SUM(IF(ISNUMBER(C2:C7), 1, 0))+1)), 0))
If #Dan's solution works, go with that since formula solutions are usually cooler :) If you need to use VBA, you can try this:
Sub Common()
Dim Date1 As Range
Dim Date2 As Range
Dim CommonDates() As Variant
Dim UniqueDates As New Collection
Set Date1 = Range("A2:A6")
Set Date2 = Range("B2:B6")
' Change the max array size to equal the length of Date1
' This is arbitrary and could be more efficient, for sure :)
ReDim CommonDates(Date1.Count)
' Set a counter that will increment with matches
i = 0
' Since a match needs to be in both, iterate through Date1 and check
' if the Match function returns a True value when checking Date2.
' If so, add that value to the CommonDates array and increment the counter.
For Each DateVal In Date1
If IsError(Application.Match(DateVal, Date2, 0)) = False Then
CommonDates(i) = DateVal.Value
i = i + 1
End If
Next
' Filter out dupes (in case that is possible - if not, this can be removed
' and the bottom part reworked
On Error Resume Next
For Each Value In CommonDates
UniqueDates.Add Value, CStr(Value)
Next Value
' Now go to the first cell in your Common Dates range (I'm using C2) and
' print out all of the results
Range("C2").Activate
For j = 1 To UniqueDates.Count
ActiveCell.Value = UniqueDates(j)
ActiveCell.Offset(1).Activate
Next j
' Back to the beginning
Range("C2").Activate
' Use this if you don't need to filter dupes
'For Each r In CommonDates
' ActiveCell.Value = r
' ActiveCell.Offset(1).Activate
'Next
End Sub
It basically iterates over Date1 and checks if the Match formula succeeds/fails in Date2. A success = match, which means a common date. Those are then printed to another column. Hope this helps!

Resources