I have been working with conditional formatting and I thought how it would look like if replaced with a "manual" comparison in VBA.
Let's say I want to compare cells between Row 1 and Row 2, meaning I compare A1 to A2, B1 to B2 etc. Whenever the value in row 2 is bigger, I want to highlight this in row 2.
If I don't want to do this with conditional formatting, how do I go about this? Do I have to loop through the cells to compare or is there a way to do it without a loop? With a loop it should look like this:
Option Explicit
Sub Testing()
Dim ws As Worksheet
Dim i As Long
Dim rng As Range
Set ws = ThisWorkbook.ActiveSheet
Set rng = ws.Range("A2:E2")
For i = 1 To rng.Count
If ws.Cells(2, i).Value > ws.Cells(1, i).Value Then
ws.Cells(2, i).Interior.ColorIndex = 44
End If
Next
End Sub
Is there a way to use less code to achieve the same result? I'm just wondering if I'm unaware of some smarter, alternative way to go about this.
"Is there a way to use less code to achieve the same result?"
Wouldn't recommend this for multiple reasons, but it can be done in a one-liner:
With Sheet1
.Range(Join(Filter(.[IF(A2:E2>A1:E1,CHAR(COLUMN(A2:E2)+64)&2,"%")], "%", False), ",")).Interior.ColorIndex = 44
End With
Why does this work:
.[IF(A2:E2>A1:E1,CHAR(COLUMN(A2:E2)+64)&2,"%")] is short for the Application.Evaluate method. Anything between .[..] is basically a Worksheet.Function where VBA is smart enough to know I want to return an array of results. The . in front would make this refer to Sheet1. Resulting array > {"A2","B2","%","%","E2"}
Filter function (not really known) would take this array, and output an resulting array, filtering out the "%" values. Hence the FALSE parameter. Resulting array > {"A2","B2","E2"}
Then the Join function would concatenate this array into a string using a "," as delimiter. Rather basic, resulting in "A2,B2,E2"
This, on its turn, is a valid Range.Address we can use withing the Range(...) reference. Once we have our Range object we can set it's Interior property with the intended ColorIndex value.
Why would I not recommend this:
While readability may already be an issue, .[..] does not take variables, meaning a dynamic array will need to be written with the .Evaluate(..) method instead, including variables, extra quotes etc. adding to the issues with readability.
While VBA is smart enough to recognize we need an array returned, this may become sluggish on large Range object. A small one like in the example is no problem at all though.
Range address can have a maximum of 255 characters. Larger arrays mean larger strings, meaning a larger change this is going to return an error at some point.
There are ways to overcome the above, but really would be too much effort preventing the obvious > Use the build-in conditional formatting instead (or if you must, your current code is nice and clean too).
Related
What ways are there to test an Excel VBA range variable for references to entire columns?
I'm using Excel 2007 VBA, iterating through Range variables with For-Each loops. The ranges are passed into the function as parameters. References to individual cells, ranges of cells, and entire rows are fine.
For instance, these are okiedokie:
Range("A1") 'One cell
Range("A1:D4") 'Range of cells.
Range("10:20") 'Entire rows 10 through 20.
But if any of the ranges have references to entire columns, it will drag the function down to a screeching halt. For instance, these are not okiedokie, and they need to be tested for and avoided:
Range("A:A")
Range("A:Z")
Range("AA:ZZ")
There are a few ways I've throught of to do this, each of them plausible but with weaknesses. The code contains loops which are used for searching through cells in worksheets with many thousands of rows, so speed is critical.
Here are three ways I can think of, but I'd like to know if there are others..?
The simplest & fastest method is to count the rows. If Range(x).Rows.Count=1048576, that's the maximum number of rows in a worksheet. However, this wouldn't work if the actual number of rows turned out to be exactly that number, or if by some wild chance there were multiple overlapping areas/ranges
that all added up to that number. Both unlikely, but possible. Also, if the version of Excel changes, so might that number, thus rendering the code broken.
Use a RegEx match against the text of Range.Address(False,False) with a pattern such as ([A-Z]{1,3}):([A-Z]{1,3}). I think this would be a medium on the speed scale.
Use VBA loops, If-Then, and string functions such as InStr() and Mid() to pick at the text of Range.Address(False,False). I think this would be the slowest possible way to do it.
You could test if the range is a reference to a column by checking the Range.Address against the Range.EntireColumn.Address like this:
If Range("AA:ZZ").Address = Range("AA:ZZ").EntireColumn.Address Then
'This returns True
End If
If Range("AA1:ZZ4").Address = Range("AA1:ZZ4").EntireColumn.Address Then
'This returns False
End If
Not sure I understand the question completely but this might work for you:
Public Sub Test()
Debug.Print RowCheck(ThisWorkbook.Worksheets("Sheet1").Range("A1:A10"))
End Sub
Public Function RowCheck(InputRange As Range)
Dim u As Long 'used number of rows
Dim x As Long 'max number of rows for any column
Dim r As Long 'number of rows based on input range
With InputRange
u = Cells(Rows.Count, .Columns(1).Column).End(xlUp).Row
r = .Rows.Count
x = Rows.Count
End With
If r = x And u < r Then
RowCheck = "A bad column reference provided"
Else
RowCheck = "This is a valid reference"
End If
End Function
Ok, after reading everyone's suggestions, I realized that no matter what I do, any Range objects passed to my function might include either an entire column reference or any combination of overlapping Range references that result in an entire column being selected.
But in translation, that means...all rows in the data, aka the UsedRange. It's possible with a large amount of data the UsedRange may actually hit the last row at 1048576. And any combination of Range references passed to my Function might result in a huge area that does cover an entire column, all the way to the maximum row.
Of course the likelihood of that happening is very low, but I do like to cover all bases in my code. But the key to this puzzle is UsedRange. This creates a "synthetic maximum last row". If the GrandRange, for lack of a better name, covers all rows in the UsedRange, then my function has nothing to do and no data to return. And so a simple IF-Then-Exit should give me the solution I was looking for:
If Intersect(UsedRange,LeGrandeRange).Rows.Count = UsedRange.Rows.Count Then
'All rows in `UsedRange` are affected.
'Nothing to do.
Exit Function
Else
'Do everything here.
'Then exit normally.
...
...
...
Endif
I have an excel table were A1 = "YES", B1 = "YES" and C1 = "YES". I want to know how many "YES" are in my table, which is easily solved with =COUNTIF(A1:C1,"YES") and it will accurately give me the answer of 3.
A B C
1 YES YES YES
But if want to know how many "YES" are in A1 and C1 and ignore whatever B1 has it becomes tricky. The same function gives me 3 while I want it to give me 2 as an answer since I want to count only A1 and C1.
I want to know if there is a way to manage data so I can work only with that kind of non-consecutive cells? I found that a solution would be using something like =if(A1="YES",1)+if(C1="YES",1) and it works like a charm since it will always give me the right answer; however, that is not a satisfying solution because despite the simplicity in my example, my real situation requires to write around 100 cells from a 500 range for several different combinations which can become somewhat heavy.
I tried using name ranges but it seems the if functions doesn't let me use them the way I want. So any help will be apreciated, thanks.
If VBA is an option and "non-consecutive ranges in excel functions" is the question, then you could create a User Defined Function which takes a ParamArray and returns an array out of all given parameters.
Example
Public Function getMatrix(ParamArray aValues() As Variant) As Variant
Dim i As Long
Dim aReturnValues() As Variant
For Each vValue In aValues
For Each vSingleValue In vValue ' possible the vValue is also an array
ReDim Preserve aReturnValues(i)
aReturnValues(i) = vSingleValue
i = i + 1
Next
Next
getMatrix = aReturnValues
End Function
This could then be used like:
Formula in A4:
=SUMPRODUCT(--(getMatrix(A1,C1,E1:G1,D2:E3)="Yes"))
Note, the UDF is returning an array, not a Range. Thats why we cannot use COUNTIF. COUNTIF needs a Range as first parameter.
Why looking for a formula when you have other features in Excel? Add a row (#2), and have A2=1, B2=0, C2=1 till the last column [Drag it to populate, Make sure to Copy Cells]. Then select row and see the Sum in Status Bar.
Have a look at this thread. Basically what you need is something like this:
=SUM(COUNTIF(INDIRECT({"A1:A10","C1:c10"}),"a"))
Or just use multiple COUNTIFs, and add them. (Countif(...) + countif(...) etc.)
I am trying to process each filled cell in a single column of an Excel spreadsheet using VBA. Assuming my variable thisCell is a Range object, I can use:
For Each thisCell In Range("A:A")
This is OK, but I want a variable such as ColToSort (defined as a Long) to specify the column, not a string. The best I could come up with after 2 hours of searching and trying was this dog:
For Each thisCell In Range(Chr(64 + ColToSort) & ":" & Chr(64 + ColToSort))
Save me from having to create a string out of a perfectly good numeric variable here! And why is it not documented anywhere?
Dynamically pointing to columns can, initially, be very confusing. I've done all sorts of things like mapping letters to numbers and the like. Thankfully there is the columns() object, which itself is a range type. I suspect what you are looking for is something more along the lines of:
For Each thisCell In Sheet1.Columns(1).Cells
You can dynamically set the column number without having to muck around with figuring out which letter it is using this method.
I've been tearing my hair out over this and after endless searching online still can't find the answer.
Here's a quick summary of what I'm looking for an a longer explanation below!
I need a VBA function to replicate a formula similar to this: {=product(A:A+1)}. This goes through every cell in column A where there is a number and adds 1, then multiplies. This can be done using either (1) Evaluate unless there are blanks or text in the range, which gives an error or (2) worksheetfunction.product, but it doesn't like it when I look through the range and add 1.
If anyone can figure out a solution for using either of these options I would be over the moon. I may also be missing something very basic!
Longer version...
What I'm trying to do:
Create three VBA functions that calculate 1) compound return 2) annualised compound return and 3) annualised volatility. These would be for variable ranges and for the annualised functions you can also specify whether it is days, months or years. For example, the compound return function when used would look something "=compoundreturn(range,number of periods)"
The calculations are easy using formula but I want to create functions to make it a quick process.
I can get all three to work through VBA using EVALUATE.
They look like this,
Function PerRet(rng As range)
PerRet = Worksheet.Evaluate("=exp(sumproduct(ln(" & rng.Address & "+1)))-1")
End Function
Function AnnRet(rng As range, np As Double)
AnnRet = Evaluate("=exp((Average(ln(" & rng.Address & "+1)))*" & np & ")-1")
End Function
Function AnnVol(rng As range, np As Double)
AnnVol = Worksheet.Evaluate("=stdev.s(ln(" & rng.Address & "+1))*sqrt(" & np & ")")
End Function
The problem is that the range selected may sometimes by the entire column, which contains blanks and potentially text. Functions like AVERAGE or STDEV.S will ignore these when you are just using them in formulas. Unfortunately, they don't do this when using Evaluate.
I know that instead of using Evaluate for AVERAGE, I could use worksheetfunction.average, which would then ignore the blanks and text. But here I come across another problem in that part of the function has to look through a range of returns (postiive and negative) and add 1 to each before averaging. This doesn't seem to work for a range unless that range is simply one cell.
An example of what I have so far for this is
Function AnnRet(rng As range, np As Double)
AnnRet = exp(WorksheetFunction.Average(WorksheetFunction.Ln(rng + 1)) * np) - 1
End Function
Any ideas how I can make it add 1 to each of the cells in the range before getting the Ln and then averaging?
Many thanks for looking
Duncan
A little light testing suggests something along these lines might work:
Sub Tester()
Dim v
v = ActiveSheet.Evaluate("=SUM(IF(ISBLANK(A:A),false,A:A+1))")
Debug.Print v
End Sub
I just had a few numbers in ColA and it only added (number+1) where the cell was populated - ie. it did not also sum up a bunch of 1's where cells were empty.
EDIT:
After your comments about problem with non-numeric cells, a little more testing comes up with something like:
Function Tester(rng As Range)
Dim v, f
f = "=AVERAGE(IF(ISBLANK({a}),FALSE,IF(ISNUMBER({a}),LN({a}+1),FALSE)))"
'remove any unsed part of the input range (for performance)
Set rng = Application.Intersect(rng, rng.Parent.UsedRange)
v = rng.Parent.Evaluate(Replace(f, "{a}", rng.Address(False, False)))
Tester = v
End Function
This first filters out blanks - non-blanks can then safely be passed to ISNUMBER() to filter out non-numeric cells. This is not quite at your final formula but not far off. Do you have some inputs and expected output for testing?
EDIT 2:
Using Intersect() to restrict a full-column input range to only the "used" part of the sheet seems to improve performace significantly (by ~100x when I only had a thousand values in the range).
I'm looking for the most elegant way to count the same number values in a noncontiguous range (I'll refer to it as just 'range'). This is the range:
=$C$2:$C$31,$E$2:$E$31,$G$2:$G$31,$I$2:$I$31,$K$2:$K$31,$M$2:$M$31,$O$2:$O$31,$Q$2:$Q$31,$S$2:$S$7
These are the parameters:
The range contains non-adjacent columns.
The columns differ in height.
The cells in the range are either empty or contain integers.
I'm checking for how many cells equal '1', how many equal '2' etc. in the range. (Not in one go, but in seperate formulas).
I've used a named range to reference the range. I'd really like to use this named range in the formula, in one way or another.
I hope I've given you enough info... Thanks in advance!
I agree with Kartik that a VBA solution is required. However the solution offered is a little inefficient in that it loops over every cell in the ranged passed into the function. It also limits the key parameter to a range reference, and can only count up to 32767 matches. Here's an alternative addresses these shortcomings
Function CountIf_N(rng As Range, key As Variant) As Variant
Dim r As Range
Dim count As Long
count = 0
For Each r In rng.Areas
count = count + WorksheetFunction.CountIfs(r, key)
Next
CountIf_N = count
End Function
Note: assumes Excel 07 or later. If using with an ealier version replace CountIfs with CountIf
One approach is to use excel built in function Countif, but it won't work with non-contigous range. The other way (the easy way) will be to use VBA to create your own custom function, and then use it in excel.
I've presented that technique here.
Goto visual basic editor in excel by pressing Alt+F11, in the project window insert a new module and paste the below code:
Function countif_n(rng As Range, key As Range) As Integer
Dim count As Integer
count = 0
For Each cell In rng
If cell.Value = key.Value Then
count = count + 1
End If
Next cell
countif_n = count
End Function
Here rng is your non-contigous range, and key represent the "range"(cell) which contains the value you want to count. For eg., to check for 1 enter 1 in any cell lets suppose "F2", and your non-contigous range is "testrange"
Then use the above function by entering the following in any blank cell:
=countif_n(testrange, F2)
Although COUNTIF can't handle non-contiguous ranges some functions can, for example RANK and COUNT so for a range called Range this formula will give the number of instances of a number in Z2 within Range
=IFERROR(COUNT(Range)-SUM(RANK(Z2,Range,{1,0}))+2,0)
assumes Excel 2007 or later but can be amended to work in earlier versions
This doesn't quite work if there's stuff below S7 that can't be counted, but you may be able to modify. It also doesn't incorporate the named range.
=SUM(IF(MOD(COLUMN(A2:S31),2)=0,IF(A2:S31=2,1,0)))
This example counts the number of 2's.
This needs to be array-entered with ctrl-shift-enter. It's based on the fact that you're counting in every other column, at least in your example. Also, although you mention the columns are different heights, it looks like all except S are the same height. So maybe there's a way to work around that.