Archive for the ‘Range Object’ Category.

Two new range functions: Union and Subtract

This post discusses two functions I developed because of a recent need. The first is an enhanced version of the Excel Union method. The other is a Subtract function that operates on ranges.

The Union function

Those who use the Excel Application’s Union method with any sense of regularity know it doesn’t deal well with any argument being ‘nothing.’ Consequently, it is almost second nature to code


Sub RoundAboutCode()
    Dim Rng1 As Range, Rng2 As Range, Rslt As Range
    ‘…
    If Rng1 Is Nothing Then
        Set Rslt = Rng2
    ElseIf Rng2 Is Nothing Then
        Set Rslt = Rng1
    Else
        Set Rslt = Application.Union(Rng1, Rng2)
        End If
    End Sub

Recently, I found myself writing the above code for the 3rd time in a few days. Annoyed at not having modularized it years ago, I did just that. It’s below.


Function Union(Rng1 As Range, Rng2 As Range) As Range
    If Rng1 Is Nothing Then
        Set Union = Rng2
    ElseIf Rng2 Is Nothing Then
        Set Union = Rng1
    Else
        Set Union = Application.Union(Rng1, Rng2)
        End If
    End Function

With the above, one can code the below without worrying about whether Rslt or NextCell is Nothing.


        Set Rslt = Union(Rslt, NextCell)

True, unlike the Union method, the function accepts only 2 arguments. I did write a more generic function declared with a ParamArray argument only to discover that the Union method won’t accept a single variant (’argument not optional’ error) or an array (’type mismatch’ error). Maybe someone else can make the more generic case work.

The Range Subtract function

I also had reason to write a Subtract function. Given two ranges, Rng1, and Rng2, where Rng2 is a subset of Rng1, the result is Rng1 - Rng2, i.e., all those cells in Rng1 that are not part of Rng2.

I remembered a post by Tom Ogilvy from a long time ago that used the following method: In a temporary worksheet, in the range corresponding to the address of Rng1 enter some constant (say the value 1). Next, clear the cells corresponding to the address of Rng2, and finally, pick up the result with the SpecialCells method.


Function SubtractUsingWS(Rng As Range, RngToSubtract As Range)
    If Application.Intersect(Rng, RngToSubtract).Address _
        <> RngToSubtract.Address Then Exit Function

    Dim OldEventsValue
    OldEventsValue = Application.EnableEvents
    Application.EnableEvents = False
    On Error GoTo Finally1

    With ThisWorkbook.Worksheets(”TempWS”)
    .Cells.ClearContents
    .Range(Rng.Address).Value = 1
    .Range(RngToSubtract.Address).ClearContents
    Set SubtractUsingWS = _
        Rng.Parent.Range(.Cells.SpecialCells(xlCellTypeConstants).Address)
        End With
    ThisWorkbook.Saved = True
Finally1:
    Application.EnableEvents = OldEventsValue
    End Function

Sub testSubtract2()
    MsgBox SubtractUsingWS(Range(”a1:C3″), Range(”b2″)).Address
    End Sub

The above is probably as efficient as one can get but concerned about issues such as write privileges, multiple people accessing an add-in on a network drive, etc., I wrote up a solution from first principles, as it were.


Function SubtractFirstPrinciples(Rng1 As Range, Rng2 As Range) As Range
    On Error Resume Next
    If Application.Intersect(Rng1, Rng2).Address <> Rng2.Address Then _
        Exit Function
    On Error GoTo 0
    Dim aCell As Range
    For Each aCell In Rng1
        Dim Rslt As Range
        If Application.Intersect(aCell, Rng2) Is Nothing Then
            Set Rslt = Union(Rslt, aCell)
            End If
        Next aCell
    Set SubtractFirstPrinciples = Rslt
    End Function

Sub testSubtractFirstPrinciples()
    Debug.Print SubtractFirstPrinciples( _
        Sheets(1).Range(”A1:f10″), _
        Sheets(1).Range(”A1,b2,c3,d4:e5,f6″)).Address
    End Sub

The advantage of working from first principles is that it works correctly irrespective of the shape of the two arguments Rng1 and Rng2. We don’t have to worry about whether they consist of multiple areas or not. The disadvantage, of course, is that it checks each cell in Rng1 and consequently might be slow under certain circumstances.

Before proceeding further, one should remember that the need for any optimization is unproven. I don’t know what, if any, problems the first solution will run into nor do I know how slow the solution based on first principles will be. So, the benefits of the optimizations below are somewhat uncertain. By contrast, it is certain that there will be some cost to developing the code, testing it, and maintaining it.

The first step in optimization would be to start small: a single area from which we want to subtract a single area. Clearly, in this case the result will be at the most four ranges as shown below. The first image shows the area that we want to subtract in yellow. The second image shows the four areas that will remain after the subtraction operation is completed.
Rng1 is the entire range shown; Rng2, the range we want to subtract, is in yellow.
The result of the subtraction is four ranges. It would be three if the range to be subtracted touched one of the edges, and just two if it was in a corner.

The code below is a function that accepts two range arguments Rng1 and Rng2 and returns a range that corresponds to Rng1 - Rng2. It validates that each range consists of a single area. I don’t know what it means to subtract Rng2 from Rng1 if there is absolutely no overlap between the two ranges. So, I made the assumption that the result should be Rng1 itself. Before proceeding with the analysis, the code computes what part of Rng2 is actually within Rng1. Each of the four If statements enclose a block of code that calculates one of the four possible ranges in the result (see the above image). Finally, the code returns the result of the subtraction.


Function subtractOneArea(Rng1 As Range, inRng2 As Range) As Range
    If Rng1.Areas.Count > 1 Then Exit Function
    If inRng2.Areas.Count > 1 Then Exit Function
    If Application.Intersect(Rng1, inRng2) Is Nothing Then
        Set subtractOneArea = Rng1
        Exit Function
        End If
    Dim Rng2 As Range
    Set Rng2 = Application.Intersect(Rng1, inRng2)
    Dim aRng As Range, OKRng As Range, Rslt As Range, WS As Worksheet
    Set WS = Rng1.Parent
    If Rng2.Row > Rng1.Row Then
        Set Rslt = WS.Range(Rng1.Rows(1), Rng1.Rows(Rng2.Row - Rng1.Row))
        End If
    If Rng2.Row + Rng2.Rows.Count < Rng1.Row + Rng1.Rows.Count Then
        Set Rslt = Union(Rslt, _
            WS.Range(Rng1.Rows(Rng2.Row - Rng1.Row + Rng2.Rows.Count + 1), _
                Rng1.Rows(Rng1.Rows.Count)))
        End If
    If Rng2.Column > Rng1.Column Then
        Set Rslt = Union(Rslt, WS.Range(WS.Cells(Rng2.Row, Rng1.Column), _
            WS.Cells(Rng2.Row + Rng2.Rows.Count - 1, Rng2.Column - 1)))
       End If
    If Rng2.Column + Rng2.Columns.Count < Rng1.Column + Rng1.Columns.Count Then
        Set Rslt = Union(Rslt, _
            WS.Range(WS.Cells(Rng2.Row, Rng2.Column + Rng2.Columns.Count), _
                WS.Cells(Rng2.Row + Rng2.Rows.Count - 1, _
                    Rng1.Column + Rng1.Columns.Count - 1)))
        End If
    Set subtractOneArea = Rslt
    End Function

With the building block in place, writing the Subtract function to calculate Rng1 - Rng2 is a lot easier. All we need to do is accumulate the result as we loop through each area of Rng1 and subtract from it each area of Rng2.


Function Subtract(Rng1 As Range, Rng2 As Range) As Range
    On Error Resume Next
    If Application.Intersect(Rng1, Rng2).Address <> Rng2.Address Then _
        Exit Function
    On Error GoTo 0
    Dim Rslt As Range, Rng1Rslt As Range, J As Integer, I As Integer
    For J = 1 To Rng1.Areas.Count
        Set Rslt = subtractOneArea(Rng1.Areas(J), Rng2.Areas(1))
        For I = 2 To Rng2.Areas.Count
            Set Rslt = Application.Intersect( _
                Rslt, subtractOneArea(Rng1.Areas(J), Rng2.Areas(I)))
            Next I
        Set Rng1Rslt = Union(Rng1Rslt, Rslt)
        Next J
    Set Subtract = Rng1Rslt
    End Function

The code is used as in the following example:


Sub testSubtract()
    Debug.Print Subtract( _
        Sheets(1).Range(”A1:f10″), _
        Sheets(1).Range(”A1,b2,c3,d4:e5,f6″)).Address
    Debug.Print Subtract( _
        Sheets(1).Range(”A1:f9,a10:f10″), _
        Sheets(1).Range(”A1,b2,c3,d4:e5,f6″)).Address
    Debug.Print Subtract( _
        Sheets(1).Range(”$I$1:$K$4,$L$4:$N$8,$K$7:$K$13″), _
        Sheets(1).Range(”$K$4:$L$4,$K$8:$L$8″)).Address
    End Sub

At some point a variant of the above will show up in the “publications and training” section of my web site.

And, that concludes all I have to share on this subject…at least for the time being.

Transposing the Formulas in a Table of Cells

OK, time for some non Excel 2007 stuff…

Recently someone asked me if it was possible to transpose a table of cells, but in such a way that the formula of each cell is kept intact. I decided VBA was the way to go and produced a small but very useful little routine for that.

What the poster wanted is to go from:

To:

Read on here.

Regards,

Jan Karel Pieterse

JKP Application Development Services

Founding member of:
Professional Office Developers Association

Index Looping Through a Range

I want to move through the cells in a range using an index number (like in a For Next loop) without using a For Each statement. I have a range that is the Union of all the cells identified using the Find method. The variable rAllFound has the address:

?rAllFound.Address
$A$2,$A$4:$A$5,$A$7

There are four cells, as you can see. Once this range is created, I set my variable lCurrent to 1 and that allows me to work with the first cell in the range.

?rAllFound.Item(lCurrent).Address
$A$2

Now I want to increment/decrement lCurrent to use the next cell. I want this:

?rAllFound.Item(lCurrent).Address
$A$4

I get this:

?rAllFound.Item(lCurrent).Address
$A$3

These characteristics of the Range object aren’t surprising, but that doesn’t get me any closer to my goal. I could loop through rAllFound with a For Each and identify the cell I want, but I didn’t like that solution.

You’ll see in my next post that I ended up using FindNext and FindPrevious, which works just fine, but I just can’t believe there’s no way to navigate a non-contiguous range using only an index. I’m sure I’m missing something simple and obvious and I’ll appreciate it if you can set me straight.

Noncontiguous Selection

To select cells that aren't next to each other with your mouse, hold down the Control key while you select the cells. You can then, for instance, get the sum of those cells from the status bar. Can you do the same thing with just the keyboard? I can't figure out how, so I wrote a procedure to do it. The F8 key will toggle EXT mode which allows you to select a range with just the arrow keys, but I couldn't figure out how to select a noncontiguous range.

Assume you have a worksheet like this:

excel range with B2, b4, and b6 filled

and you want to know what B2+B6 equals. If you were a mouse kind of a person, you might click B2, then with the Control key depressed, click B6 and read the sum from the status bar. If you're a psycho-keyboarder, like me, you'd add this to your Personal.xls:

Public grExtend As Range
Public bExtendMode As Boolean
 
Sub ToggleExtendMode()
    'cntl+shift+e
   
    Const sMODE As String = "Extendo-Mode:  Use Control+Shift+Q to include the selection"
   
    If bExtendMode Then
        Application.StatusBar = False
        bExtendMode = False
        Set grExtend = Nothing
    Else
        If TypeName(Selection) = "Range" Then
            Application.StatusBar = sMODE
            bExtendMode = True
            Set grExtend = Selection
        End If
    End If
 
End Sub
 
Sub AddToExtend()
    'cntl+shift+q
   
    If TypeName(Selection) = "Range" And bExtendMode Then
        Set grExtend = Union(grExtend, Selection)
        grExtend.Select
    End If
   
End Sub

With Control+Shift+E, you can turn on Extendo-Mode:

status bar showing extendo mode

With Extendo-Mode on, you can add selections to the current selection simply by selecting them and pressing Control+Shift+Q (I couldn't think of a good letter for that one).

Start in B2 and press Cntl+Shft+E. B2 is added to the ExtendoRange Arrow down to B6 Press Cntl+Shft+Q and the selection become B2, B6
b2 selected b6 selected b2,b6 selected

It would be nice if I could have a visual indicator of which cells were included before more were added, but it seems like a lot of work for not that much benefit. As always, your comments are welcome. Oh yeah, and here's the answer:

status bar showing sum

AutoFill Macro

I've recently added a new macro to my Personal.xls (that's four now!). This one is to replace the cumbersome Edit > Fill > Series > Autofill (alt-e-i-s, alt-f, enter).

Sub FillSeriesAutoFill()
    If TypeName(Selection) = "Range" Then
        Selection.DataSeries , xlAutoFill
    End If
End Sub

This uses the DataSeries method of the Range object. Honestly, I expected there to be a FillAuto method, but it turns out it's called AutoFill. That really is a better name, so I don't know what I was thinking. I was probably thinking along the lines of FillDown and FillRight. I'm not sure the AutoFill method would work in this capacity, however, because I wouldn't know on which Range to perform the AutoFill. For instance, if I had the numbers 1 through 7 in A1:A7, I select A1:A10 and run the above macro to fill the series down to get 1 through 10. The equivalent AutoFill would look like

Range("A1:A7").AutoFill Range("A1:A10"), xlFillSeries

I can replace Range("A1:A10") with Selection, but I don't know how to replace Range("A1:A7"). Maybe that's why they have two methods for this. It works for me, and that's what's important. Here's a rundown of the arguments for the DataSeries method:

Rowcol: You can specify whether to fill by rows (xlRows) or columns (xlColumns). I've never changed Excel's guess in the user interface, so I didn't see the need to include my own logic in this macro. I omit the argument and take Excel's guess.

Type: This corresponds to the four option buttons on the Fill Series Dialog; Linear, Growth, Date, AutoFill. Linear is the default, which was surprising to me. I thought AutoFill would be. I'm not sure I understand what these mean, but my best guess is that AutoFill determines the proper type of fill based on the data that's already selected. That works for me most of the time.

And the rest: The Professor and Mary Ann of the DataSeries arguments. You can determine how to increment the series, when to stop it, and whether to create a trend. I always enter the first two cells which determines the increment, I stop selecting when I want the series to stop, and I leave the trend setting to Old Navy.

Kindergarten Math

My wife and I split duties when it comes to teaching our kindergarten-age son. She teaches him reading and writing and I teach him math. To that end, I created a table to help him memorize adding and multiplying zero through nine. It's nothing fancy, but you can have it if you like.

Download MathGames.zip

I started with a grid and some formatting. I use Row 1 and Column A to hold random numbers so I can sort Row 2 and Column B. The data entry grid is conditionally formatted to show green for correct answers and red for incorrect.

conditional formatting dialog

The formula for the first one is

=IF($H$13=1,AND(NOT(ISBLANK(C3)),C3=C$2+$B3),AND(NOT(ISBLANK(C3)),C3=C$2*$B3))

The second formula is the same except for it's 'not equal' instead of 'equal'.

user interface with selected data shown

The Start Over button just resets everything with a simple macro:

Sub Reset()
   
    Dim rTop As Range
    Dim rSide As Range
    Dim rData As Range
   
    Set rTop = Sheet1.Range("TopRow")
    Set rSide = Sheet1.Range("SideCol")
    Set rData = Sheet1.Range("Data")
   
    rData.Parent.Unprotect
    rData.ClearContents
    rTop.Sort rTop.Cells(1), , , , , , , xlNo, , , xlSortRows
    rSide.Sort rSide.Cells(1), , , , , , , xlNo, , , xlSortColumns
    rData.Parent.Protect
   
    rData(1).Select
   
End Sub

I need to put some fancy animation in for when all the cells are green. So that there's a prize for completing it.