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"の外側の範囲を与えられたときにうまく動作しないと言う不具合があります。解決法は目下思案中…。

2010年6月15日火曜日

【Excel】半角と全角の変換マクロ⑧【VBA】

 要するにやりたかったのはこれです。

 セルを全選択しても、数十万個のデータに対して処理をかけても、数秒で完了します。
Sub 全角に変換する()
    t = Timer
    Call xStrConv(ReduceRangeR(ReduceRangeC(Selection)), vbWide)
    Application.StatusBar = " 全角に変換しました。 " _
                            & Timer - t & " 秒"
End Sub

Sub 半角に変換する()
    t = Timer
    Call xStrConv(ReduceRangeR(ReduceRangeC(Selection)), vbNarrow)
    Application.StatusBar = " 半角に変換しました。 " _
                            & Timer - t & " 秒"
End Sub

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
            GoTo xxx
        End If
    Next c
xxx:
    
    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
            GoTo xxx
        End If
        r = Cells(r2, c).End(xlUp).Row
        If r > NewRow Then
            NewRow = r
        End If
    Next c
xxx:

    Set ReduceRangeR = Range(Cells(r1, c1), Cells(NewRow, c2))
End Function

Private Sub xStrConv(a1, a2)
    If Not TypeName(a1) = "Range" Then
        MsgBox ("セルを選択して下さい")
        Exit Sub
    End If
    
    If a1.Count = 1 Then
        x = StrConv(a1.FormulaLocal, a2)
        GoTo xxx
    End If
    
    RowsCount = a1.Rows.Count
    ColumnsCount = a1.Columns.Count
    r1 = a1.Row
    c1 = a1.Column
    r2 = RowsCount + r1 - 1
    c2 = ColumnsCount + c1 - 1
    
    x = Range(Cells(r1, c1), Cells(r2, c2)).FormulaLocal
    
    For r = 1 To RowsCount
        For c = 1 To ColumnsCount
            x(r, c) = StrConv(x(r, c), a2)
        Next c
    Next r

xxx:
    a1.Value = x
End Sub

2010年6月14日月曜日

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

 昨日は、引数Rangeをとり、下方の空白行を除いたRangeを返す関数を作ったので、今回は、右方の空白行を除いたRangeを返す関数を作ってみました。
Sub HowToUse()
    ReduceRangeC(Selection).Select
End Sub

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
            GoTo xxx
        End If
    Next c
xxx:
    
    Set ReduceRangeC = Range(Cells(r1, c1), Cells(r2, NewColumn))
End Function

2010年6月13日日曜日

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

 "Range("A65536").End(xlUp).Row"で、データが入力されている最終行を取得するのはExcelVBAの定石なので、これを汎用性のある関数にしてみました。
Sub HowToUse()
    ReduceRangeR(Selection).Select
End Sub

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
            GoTo xxx
        End If
        r = Cells(r2, c).End(xlUp).Row
        If r > NewRow Then
            NewRow = r
        End If
    Next c
xxx:

    Set ReduceRangeR = Range(Cells(r1, c1), Cells(NewRow, c2))
End Function
 引数にRangeをとって、下方の空白行を除いたRangeを返します。

 実行前

 実行後

【Excel】半角と全角の変換マクロ⑦【VBA】

 疲れたので、きちんとした速度測定は次回(笑)。でも多分更に高速化されているはず。
Sub 全角に変換する()
    If Not TypeName(Selection) = "Range" Then
        MsgBox ("セルを選択して下さい")
        Exit Sub
    End If
    
    t = Timer
    
    Call ExStrConv(Selection, vbWide)
    
    Application.StatusBar = _
        ("全角に変換しました。 " & Timer - t & " 秒")
End Sub

Sub 半角に変換する()
    If Not TypeName(Selection) = "Range" Then
        MsgBox ("セルを選択して下さい")
        Exit Sub
    End If
    
    t = Timer
    
    Call ExStrConv(Selection, vbNarrow)
    
    Application.StatusBar = _
        ("半角に変換しました。 " & Timer - t & " 秒")
End Sub

Private Sub ExStrConv(a1 As Range, a2)
    If a1.Count = 1 Then
        '選択セルが一つだけの場合の処理
        'x As String
        
        x = StrConv(a1.FormulaLocal, a2)
    
    Else
        '選択セルが二つ以上の場合の処理
        'x As Variant()
        
        With a1
            r1 = .Row
            c1 = .Column
            r2 = .Rows.Count + r1 - 1
            c2 = .Columns.Count + c1 - 1
            RowsCount = .Rows.Count
            ColumnsCount = .Columns.Count
        End With
        
        x = Range(Cells(r1, c1), _
                  Cells(r2, c2)).FormulaLocal
        
        For r = 1 To RowsCount
            For c = 1 To ColumnsCount
                x(r, c) = StrConv(x(r, c), a2)
            Next c
        Next r
    
    End If
    
    a1.Value = x
End Sub

【Excel】半角と全角の変換マクロ⑥【VBA】

 ".Rows.Count"と".Columns.Count"を何度も呼ばずに、あらかじめ一度だけ変数に入れておけば少しは速くなる?と思って試してみました。
Option Base 1

Sub 全角に変換する()
    Call xStrConv(Selection, vbWide)
End Sub

Sub 半角に変換する()
    Call xStrConv(Selection, vbNarrow)
End Sub

Private Sub xStrConv(a1, a2)
    Max = 655360 '10列分のセル数
    
    If Not TypeName(a1) = "Range" Then
        MsgBox ("セルを選択して下さい")
        Exit Sub
    ElseIf a1.Count > Max Then
        MsgBox ("処理出来るデータの数は" & Max & "までです")
        Exit Sub
    End If
    
    rEnd = a1.Rows.Count
    cEnd = a1.Columns.Count
    
    Dim x: ReDim x(rEnd, cEnd)
    For r = 1 To rEnd
        For c = 1 To cEnd
            x(r, c) = StrConv(a1(r, c).FormulaLocal, a2)
        Next c
    Next r
    a1.Value = x
End Sub
 何もデータの入っていないシートのセルを全選択して計測。

書き換え前
全角:124.6094秒
半角:124.0938秒

書き換え後
全角:121.5秒
半角:121.3594秒

 うわ、微妙www。

【Excel】半角と全角の変換マクロ⑤【VBA】

 一晩寝て見直してみたら、昨日のコードよりも一昨日のコードのほうがシンプルでいいかな、と言う気がしてきたので、コードの基本的な骨格は一昨日のものを流用しつつ、配列を使って高速化してみました。
Option Base 1

Sub 全角に変換する()
    Call xStrConv(Selection, vbWide)
End Sub

Sub 半角に変換する()
    Call xStrConv(Selection, vbNarrow)
End Sub

Private Sub xStrConv(a1, a2)
    Max = 655360 '10列分のセル数
    
    If Not TypeName(a1) = "Range" Then
        MsgBox ("セルを選択して下さい")
        Exit Sub
    ElseIf a1.Count > Max Then
        MsgBox ("処理出来るデータの数は" & Max & "までです")
        Exit Sub
    End If
    
    Dim x: ReDim x(a1.Rows.Count, a1.Columns.Count)
    For r = 1 To a1.Rows.Count
        For c = 1 To a1.Columns.Count
            x(r, c) = StrConv(a1(r, c).FormulaLocal, a2)
        Next c
    Next r
    a1.Value = x
End Sub
 普段使いには一昨日のコード、大量のデータを変換したければ今回のコードを使えば良さそうです。

2010年6月12日土曜日

【Excel】半角と全角の変換マクロ④【VBA】

 文字列変換のマクロを更に改良。
 2010年6月10日木曜日のコードでも、とりあえずは完成だと思うのだけれど、せっかく名称を"xStrConv"としたのならば、"StrConv"関数と同じような呼び方が出来た方が分かりやすいです。つまり…、
    Selection.Value = xStrConv(Selection, vbWide)
みたいな呼び方が出来ればスマートかと。
 もちろん、"StrConv"関数を
    Selection.Value = StrConv(Selection, vbWide)
みたいに書くと「型が一致しません」と言うエラーが出ます。
 以下がコード。
Option Base 1

Sub 全角に変換する()
    Selection.Value = xStrConv(Selection, vbWide)
End Sub

Sub 半角に変換する()
    Selection.Value = xStrConv(Selection, vbNarrow)
End Sub

Private Function xStrConv(a1, a2)
    Dim x: ReDim x(a1.Rows.Count, a1.Columns.Count)
    For r = 1 To a1.Rows.Count
        For c = 1 To a1.Columns.Count
            x(r, c) = StrConv(a1(r, c).FormulaLocal, a2)
        Next c
    Next r
    xStrConv = x
End Function
 データを配列に格納して処理する仕様へ変更したので処理が高速です。
 処理が高速なので、たくさんのセルを選択したときにエラーを出すコードを思い切って削除しました。
 ちなみに僕の環境では、セルを全選択して実行しようとすると、「メモリが不足しています」と言うエラーが出ます。
 データを読み込む際に、値で読み込むのではなく、"FormulaLocal"を使う事により、結果として数式の入力されたセルをスキップしているかのように動作します。
 ここで、"Formula"ではなく"FormulaLocal"を使っているのがミソで、最初は"JIS"関数が"=DBCS(A1)"のようなワケの分からない文字列に化けて、ハマッてしまいました。
 救ってくれたのがこのサイト。多謝!

2010年6月10日木曜日

【Excel】半角と全角の変換マクロ③【VBA】

 文字列変換のマクロに、セル以外のオブジェクトが選択された場合と、選択セルが多すぎる場合にメッセージを表示するコードを付け加えました。
 また、引数をStrConv関数に合わせてみました。
Sub 全角に変換する()
    Call xStrConv(Selection, vbWide)
End Sub

Sub 半角に変換する()
    Call xStrConv(Selection, vbNarrow)
End Sub

Private Sub xStrConv(a1, a2)
    Max = 5000
    Set f = Application.WorksheetFunction
    
    If Not TypeName(a1) = "Range" Then
        MsgBox ("セルを選択して下さい")
        Exit Sub
    ElseIf f.CountA(a1) > Max Then
        MsgBox ("処理出来るデータの数は" & Max & "までです")
        Exit Sub
    End If
    
    For Each c In a1
        If Len(c) > 0 Then
            If Not c.HasFormula Then
                c.Value = StrConv(c, a2)
            End If
        End If
    Next
End Sub

2010年6月9日水曜日

【Excel】半角と全角の変換マクロ②【VBA】

 昨日に引き続きセルに入力された文字列を変換するマクロ。

 タイムの測定は記事がごちゃごちゃして見難くなる(笑)ので割愛。

Sub 全角に変換する()
    Call Sample(vbWide)
End Sub

Sub 半角に変換する()
    Call Sample(vbNarrow)
End Sub

Sub Sample(a)
    For Each s In Selection
        If Not Len(s) = 0 Then
            If Not s.HasFormula Then
                s.Value = StrConv(s, a)
            End If
        End If
    Next
End Sub

 列が全選択される事を想定して空白セルをスキップするコードを追加。

 ついでに、セルに数式が入力されている場合もスキップする仕様に変更。

 配列に格納するやり方は速度が魅力だけど、コードのメンテナンスのやりやすさ等、トータルに考えて、この辺りが落としどころかなぁ。

2010年6月8日火曜日

【Excel】半角と全角の変換マクロ【VBA】

 ワードには文字列を半角から全角、全角から半角に変換する機能が標準で付いていますが、エクセルには付いていません。

 ワークシート関数を使えば良いといえば良いのですが、そのためだけに使用するセルが増えて表がごちゃごちゃするのも嫌だと言う事でしょう。半角と全角を変換するマクロは割とあちこちで目にします。
Sub xxx()
    Dim t: t = Timer
    For Each c In [a1:j1000]
        c.Value = c.Address
    Next
    [a1:j1000].Select
    Debug.Print (Timer - t & "秒")
    '1.351563秒
End Sub

Sub callSample01Wide()
    Dim t: t = Timer
    Call Sample01(vbWide)
    Debug.Print (Timer - t & "秒")
    '約16秒
End Sub

Sub callSample01Narrow()
    Dim t: t = Timer
    Call Sample01(vbNarrow)
    Debug.Print (Timer - t & "秒")
    '約3秒
End Sub

Sub callSample02Wide()
    Dim t: t = Timer
    Call Sample02(vbWide)
    Debug.Print (Timer - t & "秒")
    '約2秒
End Sub

Sub callSample02Narrow()
    Dim t: t = Timer
    Call Sample02(vbNarrow)
    Debug.Print (Timer - t & "秒")
    '約0.9秒
End Sub

Sub Sample01(a)
    For Each c In Selection
        c.Value = StrConv(c, a)
    Next
End Sub

Sub Sample02(a)
    x = Selection
    For r = 1 To Selection.Rows.Count
        For c = 1 To Selection.Columns.Count
            x(r, c) = StrConv(x(r, c), a)
        Next c
    Next r
    Selection = x
End Sub
 いろいろ書いていますが、下の方のSample01とSample02がコードの本体です。

 引数として、vbNarrowやvbWideを取れるようにして、汎用性を持たせました。

 Sample01は率直ですがやや処理に時間がかかります。

 Sample02は高速に動作しますが、セル範囲が一つだけのケースでエラーが発生します。

【Excel】数式の入力されたセルに色を付ける

名前
    isFormula
参照範囲
    =GET.CELL(48,!A1)

条件付き書式
    =isFormula
 48は数式、A1の前についている!は、アクティブシートを意味するとの事。
 つまりこの式はセルが数式の場合にTrueを返します。

 "=GET.CELL(48,!A1)"でググれば情報はたくさん出てきます。

2010年6月7日月曜日

VBAでコードの実行速度を測定②

 2010年5月25日火曜日の記事で書いたコードを簡略化。
Option Explicit

Sub Sumple()
    Dim t: t = Timer

    '計測対象のコード

    Debug.Print (Timer - t & " 秒")
End Sub

2010年6月6日日曜日

【Excel】Len関数(意味のない比較だったかもw)【VBA】

 100未満の数の抽出するにあたり、値そのものを見るのと、Len関数で桁数を見るのと、どっちが速いかと思って試してみたのですが…、

Public StartTime As Double
Public EndTime As Double

Sub TimerStart()
    StartTime = Timer
End Sub

Sub TimerStop()
    EndTime = Timer
    Debug.Print (EndTime - StartTime & " 秒")
End Sub

Sub xxx()
    Call TimerStart         '---- 測定開始
    For Each c In [a1:a50000]
        c.Value = Round(Rnd() * 1000)
    Next
    [a1:a50000].Copy [b1:b50000]
    Call TimerStop          '---- 約3.42秒
End Sub

Sub SampleA()
    Call TimerStart         '---- 測定開始
    For Each c In [a1:a50000]
        If c.Value < 100 Then
            c.Value = "100以下です"
        End If
    Next
    Call TimerStop          '---- 約1.33秒(注)
End Sub

Sub SampleB()
    Call TimerStart         '---- 測定開始
    For Each c In [b1:b50000]
        If Len(c.Value) < 3 Then
            c.Value = "100以下です"
        End If
    Next
    Call TimerStop          '---- 約1.4秒(注)
End Sub

'-- (注)処理速度は生成された乱数に応じて変わりうる

 …これはお題が悪かったかなぁ。(´・ω・`)
 一応、値そのものをみるSampleAの方が僅差で速いようです。
 どちらも整数値を見て条件分岐してる訳だから、差が出ないのは当然か。

 むしろ、50,000回呼び出されているにもかかわらず、処理速度を落とさないLen関数がすごいと言えるかも。

 次回は文字列の処理に対して、
「If s = "" Then」と
「If Len(s) = 0 Then」
の違いを比較してみようかな。

EXILIM and Cyber-Shot

 最近SonyのCyber-Shotを買ったので、CASIOの昔のEXILIMと並べてみました。



 さすがに性能には大きな開きがありますが、EXILIMは動作が機敏なので、道具としての使い勝手はいまだに上々です。

 どう言う訳か僕は、新しい道具を手に入れると、古い道具を引っ張り出してきて使いたくなるようです。

2010年6月5日土曜日

【Excel】描画オブジェクトの全削除(速度測定)【VBA】

 昨日、ネットで拝借したコードの速度測定をしてみます。
Public StartTime As Double
Public EndTime As Double

Sub TimerStart()
    StartTime = Timer
End Sub

Sub TimerStop()
    EndTime = Timer
    MsgBox (EndTime - StartTime & " 秒")
End Sub

Sub Add5000Lines()
    Call TimerStart         '---- 測定開始
    For i = 1 To 5000
        ActiveSheet.Shapes.AddLine(10, i, 100, i).Select
    Next
    Call TimerStop          '---- 約0.38秒
End Sub

Sub DrawingObjectsDelete01()
    Call TimerStart         '---- 測定開始
    ActiveSheet.DrawingObjects.Delete
    Call TimerStop          '---- 約0.12秒!!!
End Sub

Sub DrawingObjectsDelete02()
    Call TimerStart         '---- 測定開始
    Set dr = ActiveSheet.DrawingObjects
    For Each d In dr
        d.Delete
    Next
    Set dr = Nothing
    Call TimerStop          '---- 約33秒
End Sub
 まずは、Add5000Lines()でラインオブジェクトを5000本引きます。
 その後に描画オブジェクトを削除するコード2種類について処理速度を比較しています。
 DrawingObjectsDelete01()は高速ですが、非表示のオブジェクトを削除出来ません。
 DrawingObjectsDelete02()は低速ですが、非表示のオブジェクトも削除してくれます。
 つまり、昨日のコードはこの二つのコードの二段構えになっています。
 素晴らしいです。

2010年6月4日金曜日

【Excel】描画オブジェクトの全削除【VBA】

Sub AllDrawingObjectsDelete()
    Set dr = ActiveSheet.DrawingObjects
    
    '-- Visible = Trueの描画オブジェクトを全削除
    dr.Delete
    
    '-- Visible = Falseの描画オブジェクトを全削除
    For Each d In dr
        d.Delete
    Next

    Set dr = Nothing
End Sub

 ここを参考にさせていただきました。

 う~ん、素晴らしいです。

2010年6月1日火曜日

【Excel】変数の型を表示させる【VBA】

 前回と同じコードをサンプルに使うのも何なので、まずは2010年5月30日日曜日のフォルダ一覧を表示させるコードをファイル一覧を表示させるコードへ変更。
Sub MakeFileList()
    Set fs = CreateObject("Scripting.FileSystemObject").GetFolder("C:\").Files
    Dim x: ReDim x(fs.Count, 1)
    For Each f In fs
        x(i, 0) = f.Name
        i = i + 1
    Next
    Range("A1:A" & fs.Count) = x
End Sub
 一行目末尾を"SubFolders"から"Files"に変更するだけでOK。


 変数の型を知りたい場合は以下のようにデバッグ用のコードを追加する。
Sub MakeFileList()
    Set fs = CreateObject("Scripting.FileSystemObject").GetFolder("C:\").Files
    Dim x: ReDim x(fs.Count, 1)
    For Each f In fs
        x(i, 0) = f.Name
        i = i + 1
    
    tn = TypeName(f)        '<-- debug
    
    Next
    Range("A1:A" & fs.Count) = x
    
    MsgBox ("i  as " & TypeName(i) & vbCrLf & _
            "x  as " & TypeName(x) & vbCrLf & _
            "f  as " & TypeName(f) & vbCrLf & _
            "fs as " & TypeName(fs) & vbCrLf & _
            "---------------------" & vbCrLf & _
            "f  as " & tn & vbCrLf & _
            "tn as " & TypeName(tn) _
            )               '<-- debug
    
End Sub
 実行すると以下のようなメッセージが出ます。

 ループ文の外で型を調べているので、普通に書くと変数"f"の型が"Empty"と表示されてしまいます。

 なので、デバッグ用にあらたに変数"tn"を用意し、ループ文の中で"f"の型を調べて、"tn"に代入した上で表示させています。

 ついでに"tn"の型も調べてみたら、"String"でした。

 この事からTypeName()関数は、変数の型を"文字列"で返す事がわかります。