Excel/VBA: Cannot get Variant/Boolean to return (should be trivial) - vba

I cannot get this function to return my values to the output column in excel:
To overcome some intense lookup tables and speed up computation, I am using a pivot table with slicers to output row numbers from filtering. These rows then need to be converted into a column of true/false cells for a large table from which I then want to perform more calculations. To avoid lookups or matching I simply need to step through the list of rows and turn those cells to "true" in the output vector.
Function IncludedinSlicer(input_range As Variant) As Variant
Dim n As Long, j As Long, r As Long
n = input_range.Height ' Height of the column of reference values
' every row in the input_range contains a row number which in the output should be TRUE
' all other rows should be false
Dim output_range As Variant
ReDim output_range(1 To 300000)
' This covers the maximum number of rows
' Initialise all rows to FALSE
For j = 1 To 300000
output_range(j) = False
Next j
' Set only those rows listed in the reference to TRUE
For j = 1 To n
r = input_range(j).Value
If r = 0 Then ' If r=0 then we are beyond the end of the reference table and have captured some blank rows
Exit For ' Exit, to avoid outside-of-array errors
Else
output_range(r) = True
End If
'End If
Next j
' Return results to Excel
' THIS LAST BIT DOES NOT RETURN VALUES TO EXCEL
IncludedinSlicer = output_range
End Function
I know this should be trivial but somehow it has vexed me for literally hours. Please help! Thank you in advance!
EDIT: Found the issue!
First, thank you for pointing out the difference between Height and Rows.Count as I was not aware of this.
Unfortunately, that still left me with the same error in the final cell output (#Value). Luckily in the meantime I tried processing this via Matlab instead and when passing back the results I got the same error. This allowed me to narrow down the problem and I tracked the error to ... (drum roll) ... VBA's 2^16 limit for array size.
My table has close to 2^18 rows so this causes the error.

input_range.Height refers to the literal height in pixels of the range. Try instead input_range.Rows.Count to get the number of rows in the range.

For your function, you should be passing in (and potentially returning) a Range type instead of the ambiguous Variant type.
This will give you direct access to all of the properties of the Range type (i.e. Rows, Columns, Cells) and would probably make it easier to catch your issue that bobajob pointed out.

Related

Comparison of two cell values always returns not equal

For whatever reason, the If zoneChanged.Columns(-5).Value <> correctZone.Columns(2).Value Then statement always evaluates as true, despite values in the cells being the same. Perhaps at this point the values are never the same, but I cannot figure out why. Also, the results that are returned alternate between two set of values even though that is not intended. Here's the code in whole:
Sub panelSearch()
Dim Pos As Long
Dim i As Long
Dim cellFieldSet As Range
Dim cellSearchSet As Range
Dim cellBeingSearched As Range
Dim cellBeingSearchedFor As Range
Dim zoneChanged As Range
Dim correctZone As Range
Dim interVal As Range
'Area I am attempting to search
Set cellSearchSet = Range("U2:U8184") '8184
'Values I am searching for
Set cellFieldSet = Range("AI2:AI615")
i = 0
For Each cellBeingSearched In cellSearchSet
For Each cellBeingSearchedFor In cellFieldSet
Pos = InStr(cellBeingSearched, cellBeingSearchedFor.Value)
If Pos > 0 Then 'code if found
Set zoneChanged = cellBeingSearched
Set correctZone = cellBeingSearchedFor
'-4142 is default color index
If zoneChanged.Columns(-5).Interior.ColorIndex = -4142 Then
'This control statement always evaluates as true even when the two cells should be the same
If zoneChanged.Columns(-5).Value <> correctZone.Columns(2).Value Then
'Need to add counter to keep multiple results from changing cell multiple times
zoneChanged.Columns(-5).ClearContents
zoneChanged.Columns(-5) = zoneChanged.Columns(-5) & correctZone.Columns(2)
zoneChanged.Columns(-5).Interior.Color = RGB(255, 0, 0)
'Counter for multiple results
If i > 0 Then
zoneChanged.Columns(-5).Interior.Color = RGB(128, 128, 128)
End If
End If
i = i + 1
End If
End If
Next cellBeingSearchedFor
Next cellBeingSearched
End Sub
Now that #DisplayName pointed out that my explanation was not correct, I got a bit deeper into it. Nevertheless the solution does not change, just my explanation why.
So the issue in the question was obviously, that it just shifted the wrong amount of columns because of (at first glance) a very odd counting system of the column property (which is not that odd if we looked into it).
Counting of Columns
When dealing with row/column counts, Excel starts counting at 1. So if we do Column(1).Select it selects the first column which is A. So the parameter we give at the column property is the column number (and not the amount we want to shift).
So because Column(1) is the first column (eg. of a selected range) that means that Column(0) is one left of the first column (of the selected range).
So if we use column for shifting .Columns(-1) shifts the selection 2 columns to the left, and yes I mean TWO.
Columns(5).Columns(-1).Select
Debug.Print Selection.Column '= 3
If we think in shifting we would await that this selects column 4 (go one left). But it selects the column number -1 where the current column is column number 1. So counting from 1 (current) to -1 (destination) it is 2 steps left of 1 and that's why it shifts 2 left.
Conclusion
I conclude that Columns() should be used to jump to a specific column number. But when we want to shift (a specific amount of columns) relatively to the current selection we should use Offset() for a convenient counting.
Counting of Offset
So Offset is shifting as expected where .Offset(0, -1) shifts selection 1 column to the left (as -1 let us expect).
To go 5 cells left use .Offset(0, -5) instead. Same for .Columns(2) should be .Offset(0, 2) accordingly.
Columns(5).Offset(0, -1).Select
Debug.Print Selection.Column '= 4
For more information look into the documentation: Range.Offset Property (Excel)
I believe that's because you're using a negative value (-5) as index for columns.
As far as I know you should use a positive number as index.

Finding and return values with associated rows on excel on multiple pages

Thanks in advance for any help! I haven't used much VBA in excel and can't work out how to do what I need, however I believe I need it to achieve the function I need within the workbook.
I have 31 data pages, in which I need to find if certain information is present and display it on a summary page.
I need is to check if there are values in the column AQ, If there is then I need the data returned in that row in columns E, F and G.
There could be multiple instances per sheet or none per sheet.
Hopefully this will example explain it better:
E F G ... AQ
Date Name Location Exception
2 1-12-17 Dave England
3 1-12-17 Sarah Wales Exp
In the example data above the information I would want returned on the Summary page is from row 3. (This type of data is on each of the 31 other pages)
Hope this makes sense! Any help on how to do this would be greatly appreciated.
There are a number of different ways you could tackle this problem, for example, pivot tables with specific filter conditions, a UDF that finds the matches and prints them to the output you'd like, etc. In general, it's not a bad idea to use the Range.Find method and loop through all the matches.
This requires a certain amount of programming time and energy, which not everyone has, although most people who use Excel a lot eventually end up using vLookup a lot. I've always been unsatisfied with vLookup, it's so limited compared to the vba Range.Find method. Just for you, since it's almost Christmas and I ran out of work that I'm actually paid to do, here's a little gem that should help solve your problem.
It's a UDF lookup that allows you specify which number match to return, and return a custom offset in rows or column to retrieve as a value. Incrementing the variable matchNum will give you all the matches in the range, and you can return whatever columns you want using the appropriate amount of offset.
The use of the Range.Find method should give you an idea of how you could use code to populate a worksheet with exactly what you wanted without using a UDF lookup function. I'll leave that as an exercise for the reader.
'################################################################################################################################
' findwhat: the value you want to find. (needle)
' where: the range you want to look for findwhat (haystack)
' matchNum: if the needle is found more than once in the haystack, find the nth target.
' rowoffset: offset your return value from the target, positive will return the value of the cell below the target.
' columoffset: offset your return value from the target, positive will return the value of the cell to the right of the target.
'################################################################################################################################
Public Function powerLookup(findwhat As Variant, where As Range, Optional matchNum As Long = 1, Optional rowOffset As Long = 0, Optional columnOffset As Long = 0) As Variant
Dim rngResult As Range, firstAddress As String
Set rngResult = Nothing
On Error GoTo Errorhandler ' if something breaks return #NA (will definitely happen if first find call fails)
Do While matchNum > 0 'loop through the matches until there are no matches or we've reached the target matchnumber
'the first time, rngResult will be Nothing, so the first find can't have rngResult as an input.
With where
If rngResult Is Nothing Then
Set rngResult = .find(findwhat, , xlValues)
firstAddress = rngResult.Address 'keep this to know if we've looped past the start
Else
'if rngResult is not nothing we've already found a match, find the next match until matchnum is 0
Set rngResult = .find(findwhat, rngResult, xlValues)
If rngResult.Address = firstAddress Then
'if we reach the first address we've looped around, no more matches found, throw #NA error
powerLookup = CVErr(xlErrNA)
Exit Function
End If
End If
End With
matchNum = matchNum - 1
Loop
powerLookup = rngResult.offset(rowOffset, columnOffset).Value 'offset the output
Exit Function
Errorhandler:
powerLookup = CVErr(xlErrNA)
End Function

Manipulating Excel spreadsheet, removing rows based on values in a column and then removing more rows based on values in another column

I have a rather complicated problem.
I have a log file that when put into excel the column "I" contains event IDs, and the column J contains a custom key that keeps a particular even grouped.
All i want to do is remove any rows that do not contain the value of say 102 in the event id column.
And THEN i need to check the custom key (column J) and remove rows that are duplicates since any duplicates will falsely show other statistics i want.
I have gotten as far as being able to retrieve the values from the columns using com objects and .entirecolumn cell value etc, but I am completely stumped as to how i can piece together a solid way to remove rows. I could not figure out how to get the row for each value.
To give a bit more clarity this is my thought process on what i need to do:
If cell value in Column I does not = 102 Then delete the row that cell contains.
Repeat for all rows in spreadsheet.
And THEN-
Read every cell in column J and remove all rows containing duplicates based on the values in column J.
Save spreadsheet.
Can any kind persons help me?
Additional Info:
Column I holds a string that is an event id number e.g = 1029
Column J holds a string that is a mix of numbers and letters = 1ASER0X3NEX0S
Ellz, I do agree with Macro Man in that your tags are misleading and, more importantly, I did indeed need to know the details of Column J.
However, I got so sick of rude posts today and yours was polite and respectful so I've pasted some code below that will do the trick ... provided Column J can be a string (the details of which you haven't given us ... see what Macro Man's getting at?).
There are many ways to test for duplicates. One is to try and add a unique key to a collection and see if it throws an error. Many wouldn't like that philosophy but it seemed to be okay for you because it also gives you a collection of all the unique (ie remaining) keys in Column J.
Sub Delete102sAndDuplicates()
Dim ws As Worksheet
Dim uniques As Collection
Dim rng As Range
Dim rowPair As Range
Dim iCell As Range
Dim jCell As Range
Dim delRows As Range
Set ws = ThisWorkbook.Worksheets("Sheet1")
Set rng = Intersect(ws.UsedRange, ws.Range("I:J"))
Set uniques = New Collection
For Each rowPair In rng.Rows
Set iCell = rowPair.Cells(, 1)
Set jCell = rowPair.Cells(, 2)
On Error Resume Next
uniques.Add jCell.Value2, jCell.Text
If Err = 457 Or iCell.Value2 = 102 Then
On Error GoTo 0
If delRows Is Nothing Then
Set delRows = rowPair.EntireRow
Else
Set delRows = Union(delRows, rowPair.EntireRow)
End If
End If
Next
If Not delRows is Nothing then
MsgBox delRows.Address(False, False) & " deleted."
delRows.Delete
End If
End Sub
There are a number of ways in which this can be done, and which is best will depend on how frequently you perform this task and whether you want to have it fully automated. Since you've tagged your question with VBA I assume you'll be happy with a VBA-based answer:
Sub removeValues()
Range("I1").Select 'Start at the top of the I column
'We are going to go down the column until we hit an empty row
Do Until IsEmpty(ActiveCell.Value) = True
If ActiveCell.Value <> 102 Then
ActiveCell.EntireRow.Delete 'Then delete the row
Else
ActiveCell.Offset(1).Select 'Select the cell below
End If
Loop
'Now we have removed all non-102 values from the column, let`s remove the duplicates from the J column
Range("A:J").RemoveDuplicates Columns:=10, Header:=xlNo
End Sub
The key line there is Range("A:J").RemoveDuplicates. It will remove rows from the range you specify according to duplicates it finds in the column you specify. In that case, it will remove items from the A-J columns based on duplicates in column 10 (which is J). If your data extends beyond the J column, then you'll need to replace "A:J" with the appropriate range. Note that the Columns value is relative to the index of the first column, so while the J column is 10 when that range starts at A (1), it would be 2 for example if the range were only I:J. Does that make sense?
(Note: Using ActiveCell is not really best practice, but it's the method that most obviously translates to what you were trying to do and as it seems you're new to VBA I thought it would be the easiest to understand).

VBA: Syntax for dynamic CountIf Ranges

I'll do my best to try and explain my problem, but it's still a bit fuzzy in my mind so this might not be as clear as it should be, for which I apologize in advance.
Here's the part of my code I'm having trouble with:
If Application.WorksheetFunction.countif(Range("D:D"), Cells(x, firstcolumn).Value) _
And Application.WorksheetFunction.countif(Range("F:F"), Cells(x, firstcolumn).Value) _
And Application.WorksheetFunction.countif(Range("H:H"), Cells(x, firstcolumn).Value) Then
The idea behind this project is to check if the values in "Cells(x, firstcolumn)" are present in columns D, F and H at the same time, and then paste the values somewhere else.
However the number of columns to check for the "Cells(x, firstcolumn)" values could be changed, so values would need to be checked in any number of columns (2, 10 etc). My code works perfectly for the specified Ranges but if one is missing or more are added then it stops working.
The columns to check against are always offset by 2 from the firstcolumn and firstcolumn is always B, it will be checked against D, F, H and so on while columns C,E,G etc have other data not relevant for this part.
My best guess is to have the countif Ranges changed dynamically but I'm at a loss of when and how this should be done...
Could anyone point me towards the right direction in order to achieve this? I can post the full code if needed.
Cheers!
You need to extract a function here. Something like this:
Private Function IsPresentInRange(ByVal source As Range, ByVal value As Variant) As Boolean
IsPresentInRange = Application.WorksheetFunction.CountIf(source, value) > 0
End Function
And then you need a way to figure out what ranges you need to give it for a source parameter - that can be a function of its own, or you can hard-code them somewhere; basically you want to have a concept of a group of ranges to call that function with - this would be the simplest:
Private Function GetSourceRanges() As Collection
Dim result As New Collection
result.Add Range("D:D")
result.Add Range("F:F")
result.Add Range("H:H")
'maintain this list here
Set GetSourceRanges = result
End Function
Ideally you would have some logic coded there, so that you don't need to manually add ranges to that collection every time.
And then you can just iterate these ranges and determine if you get a count > 0 for all of them:
Dim sources As Collection
Set sources = GetSourceRanges
Dim result As Boolean
result = True
Dim sourceRange As Range
For Each sourceRange In sources
result = result And IsPresentInRange(sourceRange, Cells(x, firstcolumn).Value)
Next
If result Then
' whatever you had in that If block
End If

Counting Rows/Columns of Selected Range Error

I am trying to determine if a selected range is within a set area... This toggles Copy/Paste restrictions in the spreadsheet. I have figured it out, I think, but I'm getting a run-time error 6 (Overflow) if you select an entire row or column. This is what I've got..
Function BETWEENROWS(ByVal Selected As Range, ByVal Min As Double, ByVal Max As Double) As Boolean
Dim LastRow As Integer
LastRow = Selected.Row + Selected.Rows.Count - 1
If BETWEEN(Min, Selected.Row, Max) = True And BETWEEN(Min, LastRow, Max) = True Then
BETWEENROWS = True
Else
BETWEENROWS = False
End If
End Function
There is one for columns BETWEENCOLUMNS as well and the function BETWEEN just returns True/False if a given number is between a min and max value.
This is working great, however, if an entire row/column is selected it's throwing an error and I'm not too familiar with VBA and the only way that I know of bypassing the error is with On Error Resume Next but that seems like I'm putting a bandaid on it and would like to figure out how to fix it another way.
Your LastRow variable is not the correct type for a number as large as the max columns/rows of the spreadsheet. Change the type to Long:
Dim LastRow As Long
You are getting an overflow error because you have made the LastRow variable an integer. Since there are more rows in an entire column then can fit in an integer variable, it triggers the overflow. You could fix this by changing the LastRow variable to be type Long
However, rather then comparing row values you may want to look into the Intersect() function. Given two (or more) ranges it will return the range object that represents the intersection of the two ranges. You could then check that intersection. If they don't intersect the range object will be Nothing. There is a good tutorial for this function at ozgrid.com
UPDATE
Here is the code to ensure range intersects fully using the Intersect() function
'// Run a test here to make sure Intersect does not return Nothing
If (TestRNG.Count <= ISectRNG.Count) And (Intersect(TestRNG, ISectRNG).Count = TestRNG.Count) Then
'// All of TestRNG falls within ISectRNG
End If