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.

11 Comments

  1. Andy Pope:

    Hi Tushar,

    How about this for a multi argument union?

    Sub TestUnion()

        Dim rngA As Range
        Dim rngB As Range
        Dim rngC As Range
        Dim rngD As Range
       
        Set rngA = Range("A1:B5")
        Set rngB = Range("A3:E4")
        Set rngD = Range("D2:D10")
       
        Union(rngA, rngB, rngC, rngD).Select
        Debug.Print Selection.Address
       
    End Sub
    Function Union(ParamArray Rngs()) As Range
       
        Dim rngUnion As Range
        Dim vntX As Variant
       
        For Each vntX In Rngs
            If Not vntX Is Nothing Then
                If rngUnion Is Nothing Then
                    Set rngUnion = vntX
                Else
                    Set rngUnion = Application.Union(rngUnion, vntX)
                End If
            End If
        Next
           
        If Not rngUnion Is Nothing Then Set Union = rngUnion

    End Function

  2. Jon Peltier:

    Minor technicality, I might have named the second function Remove or RangeRemove rather than Subtract, which is too strongly linked with the mathematical operation. The approach itself is nicely done.

  3. Tushar Mehta:

    Andy: Duh! Focused on making a single call to the Union method I overlooked the possibility of multiple calls. Thanks.

    Jon: Subtraction is a valid set operation (http://en.wikipedia.org/wiki/Set) and thanks.

  4. Matt Vidas:

    Tushar,
    Unrelated to the functions above, just wanted to send a quick hi; never realized before that we're neighbors (though I live downtown, I used to spend quite a bit of time in RV). Keep up the good work!

  5. Jon Peltier:

    Tushar -

    Good point. Maybe I'm thinking in terms of collections, where you add and remove items.

  6. keepITcool:

    Andy: You'll need more testing on the ParamArray, as users will no doubt pass the unexpected.
    For Each vRng In Rngs
    If IsObject(vRng) Then
    If Not vRng Is Nothing Then
    If TypeOf vRng Is Range Then

  7. Andy Pope:

    keepITcool: Your right I did not include code to check arguments where indeed ranges.
    But the post was just to point Tushar in the right direction ;)

  8. fzz:

    FWIW, my version of Union with argument checking.

    Function fcnUnion( _
     strict As Boolean, _
     ParamArray a() As Variant _
    ) As Range
    '-----------------------------
      Dim v As Variant, r As Range

      For Each v In a
        If Not IsObject(v) Then
          GoTo Fail

        ElseIf v Is Nothing Then
          If strict Then GoTo Fail

        ElseIf Not TypeOf v Is Range Then
          GoTo Fail

        ElseIf fcnUnion Is Nothing Then
          Set fcnUnion = v

        Else
          Set r = v
          Set fcnUnion = Union(fcnUnion, r)

        End If

      Next v

      Exit Function

    Fail:
      Set fcnUnion = Nothing

    End Function

    Arguable whether it should return an error value when it finds non-Range arguments rather than returning Nothing.

    As for the pseudo nonsymmetric set difference of a range with a single area range, i.e., removing the single area range from another, more general range, A \ B = Intersect(A, Complement(B)), and the complement of a single area range B is the union of rows above B, rows below B, columns to the left of B and columns to the right of B. Also, if A and B are disjoint, A \ B = A.

    'simple&gt; and &lt;comparisons replaced with Sgn(..) = 1
    'comparisons in order to please the fine blog software
    Function fcnRangeRemoveSA(a As Range, b As Range) As Range
      Dim i(1 To 4) As Long, w(1 To 2) As Long
      Dim ct As Range, cb As Range, cl As Range, cr As Range

      If Intersect(a, b) Is Nothing Then
        Set fcnRangeRemoveSA = a
        Exit Function
      End If

      w(1) = b.Parent.Rows.Count
      w(2) = b.Parent.Columns.Count

      i(1) = b.Row - 1
      i(2) = b.Row + b.Rows.Count
      i(3) = b.Column - 1
      i(4) = b.Column + b.Columns.Count

      With b.Parent
        If Sgn(i(1)) = 1 Then _
         Set ct = .Range(Cells(1, 1), Cells(i(1), w(2)))

        If Sgn(w(1) - i(2)) = 1 Then _
         Set cb = .Range(Cells(i(2), 1), Cells(w(1), w(2)))

        If Sgn(i(3)) = 1 Then _
         Set cl = .Range(Cells(1, 1), Cells(w(1), i(3)))

        If Sgn(w(2) - i(4)) = 1 Then _
         Set cb = .Range(Cells(1, i(4)), Cells(w(1), w(2)))

      End With

      Set fcnRangeRemoveSA = Intersect(a, _
       fcnUnion(False, ct, cb, cl, cr))

    End Function

    The complement of a multiple area range is the intersection of the complements of each area.

  9. fzz:

    Just noticed the fine blog software doesn't display the VBA keyword With in blue text. Does it not consider With a VBA keyword?

  10. fzz:

    And a bug in the 4th If statement inside the With block in the second function. It should be

    If Sgn(w(2) - i(4)) = 1 Then _
         Set cr = .Range(Cells(1, i(4)), Cells(w(1), w(2)))

  11. Dick Kusleika:

    "Does it not consider With a VBA keyword?"

    Fixed.

Leave a comment