2010年6月27日日曜日

【Excel】5,461 要素より大きな配列を返すと…【VBA】

 VBA中でワークシート関数を使用する際に、どういう条件で発生しているのかいまいち分からないエラーがあったのですが、やっと分かりました。

XL2000: カスタム関数で 5,461 要素より大きな配列を返すとエラー「#VALUE!」! マクロの実行後のエラー

…と言う事のようです。

 狭い範囲を与えた場合には正常動作するのに、範囲を広げるとエラーが出るのに気付いて、いろいろな範囲を与えながら試していたら、5,461行目を超えた所でエラーが出たので、「5461 excel」でググったら上記サイトに辿り着きました。
Sub HowToUse()
    [a1:b5461].Select
    '[a1:b5462].Selectだと型不一致エラーが出る
    
    t = Timer

    DecreaseRight(Selection).Select
    
    Application.StatusBar = _
        "処理時間 " & Timer - t & " 秒"

End Sub

Function DecreaseRight(a1 As Range) As Range
    
    With a1
        r1 = .Row
        c1 = .Column
        r2 = .Rows.Count + r1 - 1
        c2 = .Columns.Count + c1 - 1
    End With
    
    Set f = Application.WorksheetFunction
    
    NewColumn = c1
    For c = c2 To c1 Step -1
        
        'Set x = Range(Cells(r1, c), Cells(r2, c))
        Let x = Range(Cells(r1, c), Cells(r2, c))
        If f.CountA(x) > 0 Then
            NewColumn = c
            Exit For
        End If
        
    Next c

Debug.Print TypeName(x)
'Set x だとRange、Let x だとVariant()
'Variant()の場合要素数5,462以上でエラーが出る(XL2K)

    Set f = Nothing
    
    Set DecreaseRight = _
        Range(Cells(r1, c1), Cells(r2, NewColumn))

End Function
 まぁ、いつまでExcel2000使ってるつもりだよ!と言う話なんですけどねw。

2010年6月20日日曜日

【Excel】無駄な選択範囲を縮める⑥【VBA】

大量の閉じカッコで終端されて、なんだかLispっぽくなってる箇所とかありますが、とりあえず動くのでいいでしょう。
Function DecreaseRange(a1 As Range) As Range
    
    Set DecreaseRange = _
            DecreaseLeft( _
            DecreaseRight( _
            DecreaseUpper( _
            DecreaseBottom( _
            DecreaseToUsedRange(a1)))))

End Function


Function DecreaseToUsedRange(a1 As Range) As Range
    
    With a1
        r1 = .Row
        c1 = .Column
        r2 = .Rows.Count + r1 - 1
        c2 = .Columns.Count + c1 - 1
    End With
    
    With ActiveSheet.UsedRange
        ur1 = .Row
        uc1 = .Column
        ur2 = .Rows.Count + ur1 - 1
        uc2 = .Columns.Count + uc1 - 1
    End With
    
    b = (r1 > ur2) Or (c1 > uc2) Or _
        (r2 < ur1) Or (c2 < uc1) Or _
        (ur2 = 1) Or (uc2 = 1)
    If b Then
        
        'UsedRangeの外側が与えられた場合の処理
        '左上隅のセルを返す
        
        NewRow1 = r1
        NewColumn1 = c1
        NewRow2 = r1
        NewColumn2 = c1
        
    Else
    
        'UsedRangeの内側が与えられた場合の処理
        
        If r1 < ur1 Then
            NewRow1 = ur1
        Else
            NewRow1 = r1
        End If
        
        If c1 < uc1 Then
            NewColumn1 = uc1
        Else
            NewColumn1 = c1
        End If
        
        If r2 > ur2 Then
            NewRow2 = ur2
        Else
            NewRow2 = r2
        End If
        
        If c2 > uc2 Then
            NewColumn2 = uc2
        Else
            NewColumn2 = c2
        End If
        
    End If
    
    Set DecreaseToUsedRange = _
        Range(Cells(NewRow1, NewColumn1), _
              Cells(NewRow2, NewColumn2))
              
End Function


Function DecreaseBottom(a1 As Range) As Range
    
    With a1
        r1 = .Row
        c1 = .Column
        r2 = .Rows.Count + r1 - 1
        c2 = .Columns.Count + c1 - 1
    End With
    
    NewRow = r1
    For c = c1 To c2
        
        If Len(Cells(r2, c)) > 0 Then
            NewRow = r2
            Exit For
        End If
        
        r = Cells(r2, c).End(xlUp).Row
        If r > NewRow Then
            NewRow = r
        End If
        
    Next c

    Set DecreaseBottom = _
        Range(Cells(r1, c1), Cells(NewRow, c2))

End Function


Function DecreaseUpper(a1 As Range) As Range
    
    With a1
        r1 = .Row
        c1 = .Column
        r2 = .Rows.Count + r1 - 1
        c2 = .Columns.Count + c1 - 1
    End With
    
    Set f = Application.WorksheetFunction
    
    NewRow = r2
    For r = r1 To r2
        Set x = Range(Cells(r, c1), Cells(r, c2))
        If f.CountA(x) > 0 Then
            NewRow = r
            Exit For
        End If
    Next r
    
    Set f = Nothing
    
    Set DecreaseUpper = _
        Range(Cells(NewRow, c1), Cells(r2, c2))

End Function


Function DecreaseRight(a1 As Range) As Range
    
    With a1
        r1 = .Row
        c1 = .Column
        r2 = .Rows.Count + r1 - 1
        c2 = .Columns.Count + c1 - 1
    End With
    
    Set f = Application.WorksheetFunction
    
    NewColumn = c1
    For c = c2 To c1 Step -1
        Set x = Range(Cells(r1, c), Cells(r2, c))
        If f.CountA(x) > 0 Then
            NewColumn = c
            Exit For
        End If
    Next c
    
    Set f = Nothing
    
    Set DecreaseRight = _
        Range(Cells(r1, c1), Cells(r2, NewColumn))

End Function


Function DecreaseLeft(a1 As Range) As Range
    
    With a1
        r1 = .Row
        c1 = .Column
        r2 = .Rows.Count + r1 - 1
        c2 = .Columns.Count + c1 - 1
    End With
    
    Set f = Application.WorksheetFunction
    
    NewColumn = c2
    For c = c1 To c2
        Set x = Range(Cells(r1, c), Cells(r2, c))
        If f.CountA(x) > 0 Then
            NewColumn = c
            Exit For
        End If
    Next c
    
    Set f = Nothing
    
    Set DecreaseLeft = _
        Range(Cells(r1, NewColumn), Cells(r2, c2))

End Function
【2011/06/01追記】
 DecreaseToUsedRange関数で、一行目のみ或いは一列目のみを選択した際にA1セルのみを返してしまうと言うバグを発見。「ur2 = 1」と「uc2 = 1」と言う判定ロジックがまずかった。判定ロジックを「a1.Address = "A1"」に変えればOK。修正版のコードは後日…(^_^;)

2010年6月19日土曜日

【Excel】無駄な選択範囲を縮める⑤【VBA】

 無駄な選択範囲を縮める関数として、

ReduceRangeR -> 下方の無駄な選択範囲を縮める
ReduceRangeC -> 右方の無駄な選択範囲を縮める
xUsedRange -> "UsedRange"の外側の範囲を縮める

を定義しました。

 上記三つを更にまとめて、関数"ReduceRange"を定義します。
Sub HowToUse()
    ReduceRange(Selection).Select
End Sub

Function ReduceRange(a1 As Range) As Range
    'ただ単に三つの関数を束ねるだけの関数
    'xUsedRangeを最初に適用すると最も効率が良い(多分)
    Set ReduceRange = ReduceRangeR(ReduceRangeC(xUsedRange(a1)))
End Function

Private Function xUsedRange(a1 As Range) As Range
    With a1
        r1 = .Row
        c1 = .Column
        r2 = .Rows.Count + r1 - 1
        c2 = .Columns.Count + c1 - 1
    End With
    
    With ActiveSheet.UsedRange
        ur1 = .Row
        uc1 = .Column
        ur2 = .Rows.Count + ur1 - 1
        uc2 = .Columns.Count + uc1 - 1
    End With
    
    If (r1 > ur2) Or (c1 > uc2) Or (r2 < ur1) Or (c2 < uc1) Or (ur2 = 1) Or (uc2 = 1) Then
        'UsedRangeの外側が与えられた場合の処理
        '(左上隅のセルを返す)
        NewRow1 = r1
        NewColumn1 = c1
        NewRow2 = r1
        NewColumn2 = c1
        
        GoTo xxx
    End If
    
    If r1 < ur1 Then
        NewRow1 = ur1
    Else
        NewRow1 = r1
    End If
    
    If c1 < uc1 Then
        NewColumn1 = uc1
    Else
        NewColumn1 = c1
    End If
    
    If r2 > ur2 Then
        NewRow2 = ur2
    Else
        NewRow2 = r2
    End If
    
    If c2 > uc2 Then
        NewColumn2 = uc2
    Else
        NewColumn2 = c2
    End If
    
xxx:
    Set xUsedRange = Range(Cells(NewRow1, NewColumn1), Cells(NewRow2, NewColumn2))
End Function

Private Function ReduceRangeC(a1 As Range) As Range
    r1 = a1.Row
    c1 = a1.Column
    r2 = a1.Rows.Count + r1 - 1
    c2 = a1.Columns.Count + c1 - 1
    
    Set f = Application.WorksheetFunction
    
    NewColumn = c1
    For c = c2 To c1 Step -1
        If f.CountA(Range(Cells(r1, c), Cells(r2, c))) > 0 Then
            NewColumn = c
            Exit For
        End If
    Next c
    
    Set ReduceRangeC = Range(Cells(r1, c1), Cells(r2, NewColumn))
End Function

Private Function ReduceRangeR(a1 As Range) As Range
    r1 = a1.Row
    c1 = a1.Column
    r2 = a1.Rows.Count + r1 - 1
    c2 = a1.Columns.Count + c1 - 1
    
    NewRow = r1
    For c = c1 To c2
        If Len(Cells(r2, c)) > 0 Then
            NewRow = r2
            Exit For
        End If
        r = Cells(r2, c).End(xlUp).Row
        If r > NewRow Then
            NewRow = r
        End If
    Next c

    Set ReduceRangeR = Range(Cells(r1, c1), Cells(NewRow, c2))
End Function
 コードの分かりにくそうな所にコメントを少々追加。
 ついでに、"Exit For"を使うべきところで"GoTo"を使っていた箇所を修正。

 実行前

 実行後

xUsedRange
 黄、紫、青の部分までサイズを縮小

ReduceRangeC
 黄、紫の部分までサイズを縮小

ReduceRangeR
 黄の部分までサイズを縮小

 ここまで来ると、上方と左方の空白部分も最適化したくなってきます。

 要らないような気もするけど…。

【Excel】無駄な選択範囲を縮める④【VBA】

 再チャレンジ。

 "UsedRange"よりも大きな範囲が与えられた場合に"UsedRange"よりも外側の範囲を除いた"Range"を返す関数です。
Sub HowToUse()
    xUsedRange(Selection).Select
End Sub

Private Function xUsedRange(a1 As Range) As Range
    With a1
        r1 = .Row
        c1 = .Column
        r2 = .Rows.Count + r1 - 1
        c2 = .Columns.Count + c1 - 1
    End With
    
    With ActiveSheet.UsedRange
        ur1 = .Row
        uc1 = .Column
        ur2 = .Rows.Count + ur1 - 1
        uc2 = .Columns.Count + uc1 - 1
    End With
    
    If (r1 > ur2) Or (c1 > uc2) Or (r2 < ur1) Or (c2 < uc1) Or (ur2 = 1) Or (uc2 = 1) Then
        NewRow1 = r1
        NewColumn1 = c1
        NewRow2 = r1
        NewColumn2 = c1
        
        GoTo xxx
    End If
    
    If r1 < ur1 Then
        NewRow1 = ur1
    Else
        NewRow1 = r1
    End If
    
    If c1 < uc1 Then
        NewColumn1 = uc1
    Else
        NewColumn1 = c1
    End If
    
    If r2 > ur2 Then
        NewRow2 = ur2
    Else
        NewRow2 = r2
    End If
    
    If c2 > uc2 Then
        NewColumn2 = uc2
    Else
        NewColumn2 = c2
    End If
    
xxx:
    Set xUsedRange = Range(Cells(NewRow1, NewColumn1), Cells(NewRow2, NewColumn2))
End Function
 "UsedRange"の外側が与えられた場合は、その範囲の左上隅のセル一個を範囲として返す仕様にしました。
【2011/06/01追記】
 xUsedRange関数で、一行目のみ或いは一列目のみを選択した際にA1セルのみを返してしまうと言うバグを発見。「ur2 = 1」と「uc2 = 1」と言う判定ロジックがまずかった。判定ロジックを「a1.Address = "A1"」に変えればOK。修正版のコードは後日…(^_^;)

2010年6月16日水曜日

【Excel】無駄な選択範囲を縮める③【VBA】

 "UsedRange"よりも大きな範囲が与えられた場合に"UsedRange"よりも外側の範囲を除いた"Range"を返す関数です。
 2010年6月13日日曜日2010年6月14日月曜日のコードを実行する前にこのコードを実行すれば、データの有無を調べる範囲が縮まって、更なる高速化が望めるはずです。
Sub HowToUse()
    xUsedRange(Selection).Select
End Sub

Private Function xUsedRange(a1 As Range) As Range
    With a1
        r1 = .Row
        c1 = .Column
        r2 = .Rows.Count + r1 - 1
        c2 = .Columns.Count + c1 - 1
    End With
    
    With ActiveSheet.UsedRange
        ur1 = .Row
        uc1 = .Column
        ur2 = .Rows.Count + ur1 - 1
        uc2 = .Columns.Count + uc1 - 1
    End With
    
    If ur1 = 1 Then
        NewRow1 = 1
    ElseIf r1 < ur1 Then
        NewRow1 = ur1
    Else
        NewRow1 = r1
    End If
    
    If uc1 = 1 Then
        NewColumn1 = 1
    ElseIf c1 < uc1 Then
        NewColumn1 = uc1
    Else
        NewColumn1 = c1
    End If
    
    If ur2 = 1 Then
        NewRow2 = 1
    ElseIf r2 > ur2 Then
        NewRow2 = ur2
    Else
        NewRow2 = r2
    End If
    
    If uc2 = 1 Then
        NewColumn2 = 1
    ElseIf c2 > uc2 Then
        NewColumn2 = uc2
    Else
        NewColumn2 = c2
    End If
    
    Set xUsedRange = Range(Cells(NewRow1, NewColumn1), Cells(NewRow2, NewColumn2))
End Function
【2010年6月19日土曜日追記】
実はこのコード、"UsedRange"の外側の範囲を与えられたときにうまく動作しないと言う不具合があります。解決法は目下思案中…。