ラベル Excel の投稿を表示しています。 すべての投稿を表示
ラベル Excel の投稿を表示しています。 すべての投稿を表示

2019年2月24日日曜日

多数のCSVファイルを集計する

CSVファイルを扱う場合、
(1)基幹システム等から対象のデータを1ファイルでドカッと落としてきてその中で処理を行うような場合と、
(2)対象となる多数のCSVファイルがあって、各ファイルの中の該当するデータを抜き出したり集計したりする場合などがあると思います。

今回は後者の方です。
方針はいろんな立て方があると思います。
エクセルで処理をするので、各CSVをワークシートに読み込んで、一つの大きな表に結合して集計するんでも良いんです。
でも、セルの読み書きって時間がかかるので、それなら、CSVを配列に直接読み込んで集計すれば良いと思うんです。
まあ、そうなっちゃうとエクセルで処理してることになるのかどうかはちょっと疑問ではありますが。
で、多数のファイルを処理する場合、リストと照らし合わせてファイルの有無のチェックをするよりは、対象ファイルを一つのフォルダに入れて、それをリスト化して片っ端に処理した方が良いように思います。

フォルダ内のファイルのフルパスを配列に読み込む


やってると、buff=Dir(hoge) Do While buff<>"" ~ buff=Dir() Loop の間に処理を書くと、ファイルの読み込みがうまくいかなくなったりしたので、一旦対象ファイルを配列に読み込むことにしました。
ここでは拡張子指定にしてるけど、ファイル名に含まれる文字列で指定する等々簡単に書き直せるので、サンプルとしてはこんなので良いように思います。
'================================================'
    Function FullNamesIntoArrayByExtension( _
        ByVal strFolder As String, _
        ByVal strExtension As String _
    ) As Variant
' --------------------------------------------- '
'   フォルダ内指定した拡張子の全ファイルの'
'   フルパスを配列に読込む'
'   指定した拡張子のファイルが無い場合は'
'   Ubound(取得した配列) = -1 を返す'
'================================================'

    Dim strFullName As String
    Dim i As Long
    Dim buff As Variant
    Dim arrFiles() As String
    
        i = 0
        strFullName = strFolder & "\" & "*." & strExtension '"
        buff = Dir(strFullName)
        
        Do While buff <> ""
            ReDim Preserve arrFiles(i)
            arrFiles(i) = strFolder & "\" & buff '"
            i = i + 1
            buff = Dir()
        Loop
        
        If (Not arrFiles) = -1 Then
            ReDim arrFiles(-1 To -1)
            FullNamesIntoArrayByExtension = arrFiles
        Else
            FullNamesIntoArrayByExtension = arrFiles
        End If
    
    End Function

CSVファイルを配列に読込む


tetsuさんのブログですごく詳しく調べてあったので、参考にして、バイナリで読み込んで配列を戻す関数を書いてみました。

CSVファイルを配列に読込む関数


'========================================'
    Function TextIntoArray( _
        ByVal pFilePath As String _
    ) As Variant
' -------------------------------------- '
'   CSVファイルを配列に読込む'
'   エラーが起きた場合は'
'   Ubound(取得した配列) = -1 を返す'
'========================================'
        Dim lFileNo     As Long
        Dim buff        As String
        Dim bBuff()     As Byte
        Dim arrBuff
        Dim i           As Long
        Dim j           As Long
        Dim lItemCnt    As Long
        Dim arrItem
        Dim arrData()
        
        ReDim arrData(-1 To -1)
        TextIntoArray = arrData
        
        On Error GoTo AbEnd
    
        lFileNo = FreeFile
        
        Open pFilePath For Binary As #lFileNo
        
        ReDim bBuff(LOF(lFileNo))
        Get #lFileNo, , bBuff
        
        Close #lFileNo
        
        buff = StrConv(bBuff(), vbUnicode)
        
        arrBuff = Split(buff, vbCrLf)
        lItemCnt = UBound(Split(arrBuff(0), ","))
        
        ReDim arrData(UBound(arrBuff), lItemCnt)
        
        For i = LBound(arrBuff) To UBound(arrBuff)
            arrItem = Split(arrBuff(i), ",")
            For j = LBound(arrItem) To UBound(arrItem)
                arrData(i, j) = arrItem(j)
            Next j
        Next i
        
        TextIntoArray = arrData
        
        Exit Function
        
    AbEnd:
        ReDim arrData(-1 To -1)
        TextIntoArray = arrData
    
    End Function

100,000のCSVファイルを集計してみる


このようなCSVファイルで、フラグ列に「S」が立っている行の2列目の値を10万ファイル分合計してみました。
テストなのでファイル名が違うだけで各ファイル内容は同じです。

10万ファイルで350秒くらい。1000ファイルくらいなら3秒程度で終わるので、割と使えると思います。

テストで使用したコード


Sub 十万ファイルの集計()

    Dim arrFiles, sFile
    Dim sFolder As String
    Dim arrData
    Dim i As Long
    Dim fSum As Single
    Dim StartTime, EndTime
    
    StartTime = Timer
    
    sFolder = "C:\Users\user\Desktop\集計テスト"
    
    arrFiles = FullNamesIntoArrayByExtension(sFolder, "csv")
    If UBound(arrFiles) = -1 Then Exit Sub
    
    For Each sFile In arrFiles
        arrData = TextIntoArray(sFile)
        
        'CSVのFlag列の値が"S"の行のフィールド1列(2列目)の値を集計する'
        For i = LBound(arrData, 1) To UBound(arrData, 1)
            If arrData(i, 0) = "S" Then
                fSum = fSum + arrData(i, 1)
            End If
        Next i
    Next sFile
    
    EndTime = Timer
    
    MsgBox "合計値は" & Format(fSum, "#,###") & vbNewLine & _
           "所要時間(秒):" & EndTime - StartTime

End Sub

2019年2月8日金曜日

Café wall illusion

先日エクセルで描いたカフェウォール錯視を、VB Scriptでエクセルに描くようにしてみました。
コードをテキストファイルにコピペして、拡張子を「.vbs」に変更してダブルクリックすると動きます。

コード


    Option Explicit

    Call Main()

'** Subroutine ********************************************'
    Sub Main()
        'エクセルの起動'
        Dim Excel_app
        Set Excel_app = CreateExcel()
        If Excel_app Is Nothing Then
            Msgbox "Excelの起動に失敗しました。"
            Exit Sub
        End If
        '定数をセット'
        Const xlWBATWorksheet = -4167
        Const xlEdgeBottom = 9
        Const xlContinuous = 1
        Const xlThin = 2
        'エクセルでの処理'
        With Excel_app
            Dim wb, ws    
            Set wb = .Application.WorkBooks.Add(xlWBATWorksheet)
            Set ws = wb.Worksheets(1)
            
            With ws
                .Name = "カフェウォール錯視"
                .Cells.ColumnWidth = 0.58
                .Range("A1:D1").Interior.Color = vbBlack
                .Range("E1:H1").Interior.Color = vbWhite
                Dim r
                Set r = .Range("A1:H1")
                '罫線の設定'
                With r.Borders(xlEdgeBottom)
                    .LineStyle = xlContinuous
                    .Weight = xlThin
                    .Color = RGB(128,128,128)
                End With
                Dim i
                '8セルを1セットにして1列作成'
                Do While i < 24
                    r.Copy r.Offset(0,8)
                    Set r = r.Offset(0,8)
                    i = i + 1
                Loop
                '1列目をずらしながら行方向にコピー'
                Set r = .Range(.Cells(1,1), .Cells(1,i*8))
                Dim j
                For i = 1 to 24
                    If (i Mod 4)=0 Then
                        j = 0
                    Else
                        j = 2 - (i Mod 2)
                    End If
                    r.Copy r.Offset(i,j)
                Next
            End With

        End With
        Msgbox "処理完了"
    End Sub

'** Function **********************************************'
'============================'
    Function CreateExcel()
'============================'
        Set CreateExcel = Nothing
        Dim objXL, i
        Set objXL = CreateObject("Excel.Application")
        i = 0
        Do While objXL Is Nothing
            i = i + 1
            If i > 5 Then
                Exit Function
            Else
                WScript.Sleep(600)
            End If
        Loop
        objXL.Visible = True
        Set CreateExcel = objXL
    End Function

2019年2月6日水曜日

VB Script でエクセルを操作する

先日の錯視のシートをスクリプトで書こうと思ったんだけど、エクセルのマクロを実行して作成するより、VBSのアイコンをダブルクリックしてエクセルが描画するほうが面白そうなので、VB Scriptでエクセルを操作する準備。
CreateObjectの部分、起動の失敗の判定の部分はこれでよいのか、ちょっと微妙。

コード

RPAのツールでVBSで機能拡張できるのがあるので、起動済みのBookも取得できると便利かな。
    Option Explicit

    Call Main1

'** Subroutine ********************************************'
    Sub Main1()
        Dim Excel_app
        Set Excel_app = CreateExcel
        If Excel_app Is Nothing Then
            Msgbox "Excelの起動に失敗しました。"
            Exit Sub
        End If
        Dim wb, ws
        Const xlWBATWorksheet = -4167
        With Excel_app
            Set wb = .Application.WorkBooks.Add(xlWBATWorksheet)
            Set ws = wb.Worksheets(1)

            ws.Name = "テスト"

        End With
    End Sub

    Sub Main2()
        Dim Excel_app
        Set Excel_app = GetExcel
        If Excel_app Is Nothing Then
            Msgbox "Excelの取得に失敗しました。"
            Exit Sub
        End If
        Dim wb, ws
        With Excel_app
            Set wb = .Application.WorkBooks(1)
            Set ws = wb.Worksheets(1)

            Msgbox ws.Name

        End With
    End Sub

'** Function **********************************************'
'============================'
    Function CreateExcel()
'============================'
        Set CreateExcel = Nothing
        Dim objXL, i
        Set objXL = CreateObject("Excel.Application")
        i = 0
        Do While objXL Is Nothing
            i = i + 1
            If i > 5 Then
                Exit Function
            Else
                WScript.Sleep(600)
            End If
        Loop
        objXL.Visible = True
        Set CreateExcel = objXL
    End Function

'============================'    
    Function GetExcel()
'============================'
        Set GetExcel = Nothing
        Dim objXL
        Set objXL = GetObject(, "Excel.Application")
        If objXL Is Nothing Then
            Exit Function
        Else
            objXL.Visible = True
            Set GetExcel = objXL
        End If
    End Function

2019年1月7日月曜日

DB接続(ADO)

ADOは、データベースや CSV ファイルからエクセルにデータを持ってくる方法で、ActiveX データ オブジェクトの各単語の頭文字です。
Access や CSV のファイルから、SQL で指定してデータを抽出できます。
表形式のデータであればエクセルのシートのデータも扱えて、同じ Book であれば、複数の表形式のデータを結合して新しい表を作成することもできます。
(但し、1フィールド当たり256文字の文字制限などがあります)

Connection ストリングを整理して、レコードセットとしてデータを取得できるように関数を書きました。
ファイル名か SQL 文でデータソースを判別するようにしました。
取得したレコードセットは、GetRows で配列に読み込んだり、CopyFromRecordset でワークシートに張ったりできます。

サンプルコード

ヘッダーの指定の仕方が YES/NO の方が良いような気もするけど、取りあえずこんな感じ。

'========================================================================='
    Function GetRecordset( _
        ByVal pSQL As String, _
        ByVal pFullname As String, _
        Optional ByVal pHeader As Boolean _
    ) As Object
'========================================================================='
    
    Dim buff            As Variant
    Dim sPath           As String
    Dim sHDR            As String
    Dim sProvider       As String
    Dim sDataSource     As String
    Dim sExtProperties  As String
    Dim objConnection   As Object
    Dim objRecordSet    As Object
    Dim ErrMessage      As String
    Dim sConString      As String
    
    
    'ADO 定数(非参照設定)'
    Const adOpenStatic = 3
    Const adLockOptimistic = 3
    Const adCmdText = 1
    
    Set objConnection = CreateObject("ADODB.Connection")
    Set objRecordSet = CreateObject("ADODB.Recordset")
    
    '必須チェック'
    Select Case True
    Case InStr(pFullname, "SQLOLEDB") = 0
        '何もしない'
    Case pSQL = ""
        ErrMessage = "SQL文が空白です"
        GoTo RaiseError
    Case pFullname = ""
        ErrMessage = "ファイルのフルパスが空白です"
        GoTo RaiseError
    End Select
    
    'ファイル存在チェック'
    If Dir(pFullname) = "" Then
        ErrMessage = "ファイルが見つかりません"
        GoTo RaiseError
    End If
    
    '下準備'
    sHDR = IIf(pHeader, "HDR=YES", "HDR=NO")
    buff = Dir(pFullname)
    sPath = Replace(pFullname, buff, "")    'CSV用ファイルパス'
     
    'データソースの判定'
    Select Case True
    Case InStr(pSQL, ".csv") > 0, InStr(pSQL, ".txt") > 0
        sExtProperties = "Text; FMT=Delimited;" & sHDR
        sDataSource = sPath
    Case InStr(pFullname, "xls") > 0
        sExtProperties = "Excel 12.0; IMEX=1;" & sHDR
        sDataSource = pFullname
    Case InStr(pFullname, "accdb") > 0, InStr(pFullname, "mdb") > 0
        sDataSource = pFullname
    Case InStr(pFullname, "SQLOLEDB") > 0
        sDataSource = "SQLServer"
        sConString = pFullname
    Case Else
        ErrMessage = "データソースを判定できません"
        GoTo RaiseError
    End Select
    
    'DB オープン'
    If sDataSource <> "SQLServer" Then
        With objConnection
            .Provider = "Microsoft.ACE.OLEDB.12.0"
            .Properties("Extended Properties") = sExtProperties
            .Properties("Data Source") = sDataSource
            .Open
        End With
    Else
        objConnection.Open sConString
    End If
    
    'RecordSet オープン'
    objRecordSet.Open pSQL, objConnection, adOpenStatic, adLockOptimistic, adCmdText
    
    'リターン'
    Set GetRecordset = objRecordSet
    
    Exit Function
    
    RaiseError:
    Err.Raise 999, "Function getRecordset", ErrMessage
    
    End Function

使用方法サンプル

    Sub testCSV()
        Dim sSQL        As String
        Dim sFullname   As String
        Dim bHeader     As Boolean
        Dim rs          As Object

        sSQL = "SELECT * FROM Test.csv"
        sFullname = "C:\Users\user\Desktop\Test.csv"
        bHeader = True

        Set rs = GetRecordset(sSQL, sFullname, bHeader)

    End Sub
    
    Sub testExcel()
        Dim sSQL        As String
        Dim sFullname   As String
        Dim bHeader     As Boolean
        Dim rs          As Object

        sSQL = "SELECT * FROM [表紙$B3:G7]"
        sFullname = "C:\Users\user\Desktop\Test.xlsm"
        bHeader = True

        Set rs = GetRecordset(sSQL, sFullname, bHeader)

    End Sub
    
    Sub testAccess()
        Dim sSQL        As String
        Dim sFullname   As String
        Dim rs          As Object

        sSQL = "SELECT * FROM Table1"
        sFullname = "C:\Users\user\Desktop\Database1.accdb"

        Set rs = GetRecordset(sSQL, sFullname)

    End Sub

    Sub testSQLServer()
        Dim sSQL        As String
        Dim sCnString   As String
        Dim rs          As Object

        sSQL = "SELECT * FROM Table1"
        sCnString = "PROVIDER=SQLOLEDB;DATA SOURCE=SQLSERVER-01;UID=UserID;PWD=PassWord;DATABASE=USER_DB"

        Set rs = GetRecordset(sSQL, sCnString)

    End Sub

2018年11月26日月曜日

マクロの一時停止

下記の 例1 のように、マクロの実行を一旦停止させる時に使いますが、そもそもは指定時刻まで停止するメソッドです。
戻り値は Boolean なので、例2 のように書いた方が間違いがないかもしれません。

サンプルコード

    Sub 例1()
        Application.Wait Now + TimeValue("0:00:05")
        MsgBox "5秒間停止しました"
    End Sub
    
    Sub 例2()
        If Application.Wait(Now + TimeValue("0:00:05")) Then
            MsgBox "5秒間停止しました"
        End If
    End Sub

2018年11月24日土曜日

書式設定

サンプルコード

    Sub 書式設定()
        With Selection
        
           '■書式設定'
            .NumberFormatLocal = "G/標準"
            .NumberFormatLocal = "@"

           '■文字の配置'
            ''横位置
            .HorizontalAlignment = xlHAlignCenter     ''右詰め:xlHAlignRight
            ''縦位置
            .VerticalAlignment = xlVAlignCenter
            ''文字の折り返し
            .WrapText = False
            ''縮小して全体を表示
            .ShrinkToFit = False
            ''セルの結合
            .MergeCells = True
            
           '■フォント'
            With .Font
                ''フォント名
                .Name = "MS Pゴシック"
                ''サイズ
                .Size = 12
            End With
            
          ''■背景色
            ''背景色の設定
            .Interior.Color = vbYellow              ''RGB(255,255,0)
            ''背景色のクリア
            .Interior.ColorIndex = xlNone
          
        End With
    End Sub

罫線の設定

サンプルコード

    Sub 罫線の設定()
        
        ''格子を設定
        Range("A1:D5").Borders.LineStyle = xlContinuous
        
        ''罫線を消す
        Range("A1:D5").Borders.LineStyle = xlLineStyleNone
        
        ''太い外枠を設定(BorderAroundメソッド)
        Range("A1:D5").BorderAround Weight:=xlMedium
        
        ''内側の縦線に細線を設定
        Range("A1:D5").Borders(xlInsideVertical).Weight = xlThin
        
        ''内側の横線に極細線を設定
        Range("A1:D5").Borders(xlInsideHorizontal).Weight = xlHairline
        
        ''表の右外に太い縦線を設定
        Range("E1:E5").Borders(xlEdgeLeft).Weight = xlMedium
        
    End Sub

2018年11月17日土曜日

全シートのセルのA1を選択する

割とよくやりがちな間違いは、各シートをループで回して、
    Workbooks(i).Worksheets(i).Activate
    Range("A1").Select
とうっかり書いちゃうんですが、これは期待した結果になりません。

どういうことかというと、実際にコードを書いて動かしてみるとわかるんだけど、シートを移動することになるので、
    Application.ScreenUpdating = False
をいれることになります。
そうすると、↓これは
こうなって↓欲しいのに
こうなっちゃいます。↓ 残念...


あと、タイトル行を固定してフィルタをかけてる表はよくありますが、これも...
こんな残念な結果になります↓(A1は選択されているけど、、スクロールしてない、、残念...。)

Application.Goto メソッド

シートの選択、セルの選択、ウィンドウ固定のスクロールを全部やってくれる、とても便利なメソッドがあるので、これを使います。

サンプルコード

選択しているシートやスクリーン更新の状態は、呼び出しの前後で変化すると使いにくくなりそうなので、元の状態に戻すようにしてます。
    Sub 全シートのA1セルを選択()
    
        Dim home As Worksheet
        Dim bScrUpdStatus As Boolean
        Dim ws As Worksheet
        
        ''初期状態を保存
        Set home = ActiveSheet
        bScrUpdStatus = Application.ScreenUpdating
        
        Application.ScreenUpdating = False
        
        With ThisWorkbook
            For Each ws In .Worksheets
                Select Case True
                Case InStr(ws.Name, "除外したいシート名") > 0
                    '何もしない'
                Case Else
                    Application.Goto reference:=ws.Range("A1"), scroll:=True
                End Select
            Next ws
        End With
        
        ''初期状態に戻す
        home.Activate
        Application.ScreenUpdating = bScrUpdStatus
    
    End Sub

2018年11月14日水曜日

アクセスのテーブルにインデックスを追加・削除する

 インデックスが振られていないアクセスのテーブルからデータを抽出していたら5分以上かかったり、メモリー不足です的な謎のエラーが頻発したり。
 じゃ、抽出する前にテーブルにインデックスを追加すればいいんじゃね?ということでググってみたら、msdnにコードが紹介されているんだけど、更新されていなくてそもそもExcel 2016では動かないし、ひとつのテーブルにひとつのインデックスを追加するという内容でイマイチ。
 複数のフィールドにインデックスを付けられた方が便利なので、フィールドを配列にした上で、このmsdnのサンプルをExcel 2016で動くように書き直しました。

インデックスの追加


呼び出し側

データは構造体にして処理ルーチンに渡すことにしました。
    Option Explicit
    
    Type DbInfo
        FullName        As String
        TableToIndex    As String
    End Type
        
    Type IndexInfo
        Field           As String
        Name            As String
        Nulls           As ADOX.AllowNullsEnum
        SortOrder       As ADOX.SortOrderEnum
    End Type

    Sub Accessのテーブルにインデックスを追加()
        
        Dim DB As DbInfo
        Dim Index(1) As IndexInfo
        
        DB.FullName = "C:\Users\user\Desktop\blog用\Database1.accdb"
        DB.TableToIndex = "Table_1"
        Index(0).Field = "性別"
        Index(1).Field = "都道府県"
        Index(0).Name = "Idx性別"
        Index(1).Name = "Idx都道府県"
        Index(0).Nulls = adIndexNullsDisallow
        Index(1).Nulls = adIndexNullsDisallow
        Index(0).SortOrder = adSortAscending
        Index(1).SortOrder = adSortAscending
        
        Call CreateIndex(DB, Index)
    
    End Sub

処理ルーチン

参照設定の ADO, ADOX のバージョンの選択については、特に裏付けはとってないです
'   参照設定                                                             '
'   Microsoft ActiveX Data Objects 6.1 Library                           '
'   Microsoft ADO Ext 6.0 for DDL and Security                           '
'========================================================================='
    Sub CreateIndex( _
        ByRef pDb As DbInfo, _
        ByRef pIdx() As IndexInfo _
    )
'========================================================================='
        Dim catDB As New ADOX.Catalog
        Dim tbl As New ADOX.Table
        Dim Idx() As ADOX.Index
        Dim i As Integer
        Dim j As Integer
    
      ''インデックスを作成するデータベースのカタログを開きます。
        catDB.ActiveConnection = "Provider=Microsoft.ACE.OLEDB.12.0;" & _
                                 "Data Source=" & pDb.FullName
        Set tbl = catDB.Tables(pDb.TableToIndex)
    
        For i = LBound(pIdx) To UBound(pIdx)
          ''フィールドにインデックスが振られているかどうかの判定
            For j = 0 To (tbl.Indexes.Count - 1)
                If tbl.Indexes.Item(j).Columns.Item(0).Name = pIdx(i).Field Then
                    Exit For
                End If
            Next j
          ''インデックスが設定されていないと、j = tbl.Indexes.Countになる
            If j = tbl.Indexes.Count Then
              ''Index オブジェクトを作成し、テーブル列に関連付けます。
                ReDim Preserve Idx(i)
                Set Idx(i) = New ADOX.Index
                With Idx(i)
                   .Name = pIdx(i).Name
                   .IndexNulls = pIdx(i).Nulls
                   .Columns.Append pIdx(i).Field
                   .Columns(pIdx(i).Field).SortOrder = pIdx(i).SortOrder
                End With
              ''Index オブジェクトを Table オブジェクトの Indexes コレクションに
              ''関連付けます。
                tbl.Indexes.Append Idx(i)
            End If
        Next i
        Set catDB = Nothing
    End Sub

インデックスの削除


呼び出し側

インデックスを削除するルーチンを追加しました(2019/01)
    Sub 追加したインデックスを削除()
        
        Dim DB As DbInfo
        Dim Index(1) As IndexInfo
        
        DB.FullName = "C:\Users\user\Desktop\blog用\Database1.accdb"
        DB.TableToIndex = "dummy"
        Index(0).Name = "Idx性別"
        Index(1).Name = "Idx都道府県"
        
        Call DeleteIndex(DB, Index)
    
    End Sub

処理ルーチン

'   参照設定                                                             '
'   Microsoft ActiveX Data Objects 6.1 Library                           '
'   Microsoft ADO Ext 6.0 for DDL and Security                           '
'========================================================================='
    Sub DeleteIndex( _
        ByRef pDb As DbInfo, _
        ByRef pIdx() As IndexInfo _
    )
'========================================================================='
        Dim catDB As New ADOX.Catalog
        Dim tbl As New ADOX.Table
        Dim Idx() As ADOX.Index
        Dim i As Integer
        Dim j As Integer
    
        catDB.ActiveConnection = "Provider=Microsoft.ACE.OLEDB.12.0;" & _
                                 "Data Source=" & pDb.FullName
        Set tbl = catDB.Tables(pDb.TableToIndex)
    
        For i = LBound(pIdx) To UBound(pIdx)
            For j = 0 To (tbl.Indexes.Count - 1)
                If tbl.Indexes.Item(j).Name = pIdx(i).Name Then
                    tbl.Indexes.Delete pIdx(i).Name
                    Exit For
                End If
            Next j
        Next i
        Set catDB = Nothing
    End Sub

2018年11月12日月曜日

エラーハンドリング

 ググっていて結構嘘が書いてあるのが On Error GoTo は一回しか効かないというもの。
 そういうサイトには、例として、On Error GoTo でエラーハンドラーに飛ばして、「GoTo ResumePoint」という形で制御を戻すと、エラーが2回目だとそこで止まっちゃうとか書いてあって、やってみると確かに止まります。
 でも、VBAのリファレンスを読むと「プロシージャ内でエラーがエラー ハンドラーにより処理された後、 Resume ステートメントで指定されたポイントから、現在のプロシージャで実行が再開されます。」って書いてあるので、「Resume ResumePoint」と書いてみると、何回でも On Error GoTo でエラーハンドラーへちゃんとジャンプします。

サンプルコード

    Sub ErrorCatch()
        On Error GoTo AbEnd
        Dim i As Integer

    ResumePoint:
        Err.Raise 800, "Test", "Error Test"    ''強制的にエラーを発生させる
        Exit Sub
        
    AbEnd:
        If Err.Number = 800 Then
            If i < 10 Then
                i = i + 1
                Resume ResumePoint    ''Resumeで処理を戻す
            End If
        End If
        MsgBox "エラー回数:" & i & "回" & vbNewLine & _
               "エラー番号:" & Err.Number & vbNewLine & _
               "エラー内容:" & Err.Description
    End Sub

結果


2018年11月6日火曜日

動的配列が初期化されているかどうかの判定

ググると 「If Not Not AnyArray Then」でFalseが返ってくれば初期化されていなくて、Trueが返ってくれば初期化済みって書いてあります。

仕組みとしては、初期化されていない動的配列について、Not演算子は-1を返すということを使っています。
配列の型にかかわらず-1が戻ってきます。

で、更に、Not演算子は整数値の-1に対しては符号を含めた論理否定演算を実行して0を返すので、VB(A)で(Not Not 未初期化配列)をIf判定すると0がBooleanに型変換されFalseとなります。
んで、繰り返しっぽくなりますが、、
初期化されている動的配列について、Not演算子は負の-1じゃない値を返します。
んでんで、-1じゃない負の数については、Not演算子は数値式の符号を含めてビット毎の論理否定演算を実行するため0以外の正の数値を返します。
VB(A)ではBooleanに0以外の数値が与えられた場合はTrueを返すので、(Not Not 初期化済み配列)をIf判定するとTrueになります。

と、まあ、説明を読んでも、誰かに説明されても正直よくわからないわけです。

で、結局、初期化されてない配列については、Not演算子が-1を返すよってのだけでよくね?って話です。

結論


(Not 動的配列) = -1 なら初期化されていません。

注意


Not 動的配列 の部分は括弧で囲わないと正しくビット毎の演算ができません、だそうです。
    Sub 動的配列の初期化判定()
        Dim arr() As 型はなんでも
        If (Not arr) = -1 Then
            ''初期化されてない
        Else
            ''初期化済み
        End If
    End Sub

2018年11月5日月曜日

日付

年度


年度の場合は和暦やxx年度といった表記になることが多いと思うので、Formatしてテキストを戻すようにするのが良いように思います。
'========================================================================='
    Function FiscalYear( _
        Optional ByVal pDate As Date, _
        Optional ByVal pFormat As String, _
        Optional ByVal pFYStartMonth As Integer _
    ) As String
''  指定日の年度を指定のFormatで返す。
''  CY内に翌FYが開始する場合は開始月にマイナスを付けて指定する
'========================================================================='
        Dim buff As Date
       '初期化'
        FiscalYear = ""
        If pDate = 0 Then pDate = Date
        If pFormat = "" Then pFormat = "yyyy"
        If pFYStartMonth = 0 Then pFYStartMonth = 4
       '年度算出'
        buff = DateSerial(Year(pDate), Month(pDate) - (pFYStartMonth - 1), 1)
       '書式を設定しリターン'
        FiscalYear = Format(buff, pFormat)
    End Function

一日、末日


指定する日をテキストで渡すんだけど、今月?当月?ってなっちゃうので、enumを設定してインテリセンスが効くようにすると便利です。
Option Explicit
Enum TheDayOfaMonth
    前月一日 = 0
    前月末日
    当月一日
    当月末日
    来月一日
    来月末日
End Enum
'========================================================================='
    Function DayOfaMonth( _
        ByVal pTheDay As TheDayOfaMonth, _
        Optional pDate As Date _
    ) As Date
'========================================================================='
        '初期化'
        DayOfaMonth = 0
        If pDate = 0 Then pDate = Date
        
        Select Case pTheDay
        Case 前月一日
            DayOfaMonth = DateSerial(Year(pDate), Month(pDate) - 1, 1)
        Case 前月末日
            DayOfaMonth = DateSerial(Year(pDate), Month(pDate), 0)
        Case 当月一日
            DayOfaMonth = DateSerial(Year(pDate), Month(pDate), 1)
        Case 当月末日
            DayOfaMonth = DateSerial(Year(pDate), Month(pDate) + 1, 0)
        Case 来月一日
            DayOfaMonth = DateSerial(Year(pDate), Month(pDate) + 1, 1)
        Case 来月末日
            DayOfaMonth = DateSerial(Year(pDate), Month(pDate) + 2, 0)
        End Select

    End Function

締日


20日締翌月末

こういうのはなかなか思いつかないですね
    DateSerial(Year(Date), Month(Date) + 2 - (day(Date) > 20), 0)

2018年10月28日日曜日

テキストの数字の最大値を取得

数値データの最大値を取得することはよくあり、Application.WorksheetFunction.max() を使えば良いですが、コード番号などで数値がテキストとして使われることがあります("001"といったものです)
下の表(ソースは「なんちゃって個人情報」)では誕生日がテキストの数字となっています。
この最大値を取得しようとしてワークシート関数のMax()を使うと、0が返ってくるという残念な結果に、、
なので、テキストとして入力されている数字の最大値を取得する関数です。

関数サンプル

    Sub 誕生日の最大値を取得()
        Dim max誕生日 As Variant
        On Error GoTo AbEnd
        max誕生日 = MaxInNumericString(Range("D2:D101"))
        MsgBox max誕生日
        Exit Sub
    AbEnd:
        MsgBox Err.Description
    End Sub
'========================================================================='
    Function MaxInNumericString( _
        ByVal pRange As Range _
    ) As Variant
''  文字列として扱われている数値データの中で最大の値を取得する
'========================================================================='
        Dim arrData     As Variant
        Dim i           As Integer
        Dim j           As Integer
        Dim MaxString   As Variant
        Dim MaxValue    As Single
        Dim Buff        As Variant
        ''初期化
        MaxInNumericString = False
        ''Rangeを2次元の配列に代入
        arrData = pRange
        ''最大値を取得
        For i = LBound(arrData, 1) To UBound(arrData, 1)
            For j = LBound(arrData, 2) To UBound(arrData, 2)
                Buff = arrData(i, j).value
                ''数字データ以外の場合は抜ける
                If IsNumeric(Buff) = False Then
                    Err.Raise _
                        800, _
                        "MaxInNumericString", _
                        "データに数字以外の文字列が含まれています"
                    Exit Function
                End If
                If i = 1 And j = 1 Then
                    MaxString = Buff
                    MaxValue = CSng(Buff)
                ElseIf MaxValue < CSng(Buff) Then
                    MaxString = Buff
                    MaxValue = CSng(Buff)
                End If
            Next j
        Next i
        
        MaxInNumericString = MaxString
    
    End Function

2018年10月26日金曜日

列番号の設定

例えば基幹システムから出力したデータCSVに、ビジネス側で持っている顧客情報や契約情報をリンクして集計して出力表を作ることがあります。
インストールされてるからアクセスで~とか思っていると、エクセルで作って下さいって言われます。
こういう場合の表データは、英訳すればそれこそテーブルな訳で、列がフィールドで行がレコードです。
なので、不定なレコード数については作業の途中で「最終行の取得」という処理が頻繁に起きますが、列については所与のものとして扱えばよい場合がほとんどだと思います。
複数の表に共通した列がある場合は区別しないといけないので、“表名_列名”にして、下のような宣言になりがちですが、なんかイマイチ。
    ''データ表列番号
    Const 名前              As Integer = 1
    Const 性別              As Integer = 2
    Const 年齢              As Integer = 3
    Const 誕生日            As Integer = 4
    Const 婚姻              As Integer = 5
    Const 都道府県          As Integer = 6
    Const 一月              As Integer = 7
    Const 二月              As Integer = 8
    Const 三月              As Integer = 9

    ''顧客表列番号
    Const 顧客_名前         As Integer = 1
    Const 顧客_性別         As Integer = 2
    Const 顧客_年齢         As Integer = 3
    Const 顧客_担当営業     As Integer = 4
    Const 顧客_管轄営業所   As Integer = 5
    
    ''集計表列番号
    Const 集計_管轄営業所   As Integer = 1
    Const 集計_担当営業     As Integer = 2
    Const 人数              As Integer = 3
    Const 第一Q売上         As Integer = 4


列挙体 enum

enum というものがあるので、モジュールの頭(Optionの次に書いとけばいいです)に下記のように書きます。
※プロシジャの中では宣言できません
※数字が連続している場合は2つ目以降は省略できます
    ''データ表列番号
    Enum データ
        名前 = 1
        性別 = 2
        年齢 = 3
        誕生日 = 4
        婚姻 = 5
        都道府県 = 6
        一月 = 7
        二月 = 8
        三月 = 9
    End Enum

    ''顧客表列番号
    Enum 顧客
        名前 = 1
        性別 = 2
        年齢 = 3
        担当営業 = 4
        管轄営業所 = 5
    End Enum
    
    ''集計表列番号
    Enum 集計
        管轄営業所 = 1
        担当営業 = 2
        人数 = 3
        第一Q売上 = 4
    End Enum

"Enum hogehoge"~"End Enum"で括られている部分はそれぞれ区別されているので、それぞれの中では「名前」など重複する変数を使うことができます。
書き方はhogehogeと変数の間にドットを入れます。
【例】顧客.名前

インテリセンス

enum では、VBEでコードを書く時にドットを入力した時点でインテリセンスが利きます。
enum を使う理由は、数字を文字列に置き換えられるのはもちろんですが、列名が他の表とかぶっていてもそのまま使えることと、このインテリセンスが使えることです。

2018年10月24日水曜日

ワークブックを開く

エクセルのファイルを開く時に、ダイアログで直接指定して開くのであればダイアログの出し方だけを気にしておけばよいですが、セル等に保存されているファイルのフルパスを読んで開く場合はファイルの存在チェックなどが必要になります。
で、ダイアログの使い方の例と、ファイルのフルパスを指定してブックを開く関数です。

ダイアログ


まず、ダイアログの書き方です。

FileDialog(msoFileDialogOpen)

    Sub Bookを開く()
        With Application.FileDialog(msoFileDialogOpen)
            With .Filters
                .Clear
                .Add "Excelブック", "*.xls; *.xlsx; *.xlsm", 1
                .Add "すべてのファイル", "*.*", 2
            End With
            .InitialFileName = ThisWorkbook.FullName
            .Title = "ファイルを開く"
            If .Show = True Then
                .Execute
            Else
                ''キャンセルボタンが押された
            End If
        End With
    End Sub

FileDialog(msoFileDialogFilePicker)

    Sub 指定したファイルを配列にセット()
        Dim i           As Integer
        Dim arrFiles()  As String
    
        With Application.FileDialog(msoFileDialogFilePicker)
            .AllowMultiSelect = True
            With .Filters
                .Clear
                .Add "Excelブック", "*.xls; *.xlsx; *.xlsm", 1
                .Add "すべてのファイル", "*.*", 2
            End With
            .InitialFileName = ThisWorkbook.FullName
            If .Show = True Then
                For i = 1 To .SelectedItems.Count
                    ReDim Preserve arrFiles(1 To i)
                    arrFiles(i) = .SelectedItems(i)
                Next
            Else
                ''キャンセルボタンが押された
            End If
        End With
    End Sub

FileDialog(msoFileDialogFolderPicker)

    Sub フォルダの取得()
        Dim sPath As String
        
        With Application.FileDialog(msoFileDialogFolderPicker)
            .InitialFileName = ThisWorkbook.Path
            .Title = "フォルダの選択"
            If .Show = True Then
                sPath = .SelectedItems(1)
            Else
                ''キャンセルボタンが押された
            End If
        End With
    End Sub

GetOpenFilename:1ファイル

    Sub ファイル名を取得()
        Dim FileToOpen As Variant
        
        FileToOpen = Application.GetOpenFilename( _
                         "Excelブック(*.xls?), *.xls?,すべてのファイル(*.*),*.*", _
                         1, _
                         "ファイル名を取得")
        If FileToOpen = False Then Exit Sub
    End Sub

GetOpenFilename:複数ファイル名を取得

    Sub 複数ファイル名を取得()
        Dim FileToOpen  As Variant
        
        FileToOpen = Application.GetOpenFilename( _
                         "Excelブック(*.xls?),*.xls?,すべてのファイル(*.*),*.*", _
                         2, _
                         "ファイル名を取得", , True)
        If VarType(FileToOpen) = vbBoolean Then
            If FileToOpen = False Then Exit Sub
        Else
            ''FileToOpen(配列、インデックス:1ベース)でFullNameを取得できる
        End If
    End Sub

関数サンプル


ファイルのフルパスを渡して開いて、ワークブックオブジェクトを戻すようにしました。
    Sub OpenBookの呼び出し()
        Dim sfile
        Dim wb As Workbook
        On Error GoTo AbEnd
        sfile = "ファイルのフルパス"
        Set wb = OpenBook(sfile)
        Exit Sub
    AbEnd:
        MsgBox Err.Description
    End Sub
'========================================================================='
    Function OpenBook( _
        ByVal pBookFullNameToOpen As String _
    ) As Workbook
'========================================================================='
        
        Dim home As Workbook
        Dim ScrUpdStatus As Boolean
        Dim Buff As String
        Dim wb As Workbook
        
        ''初期化
        Set OpenBook = Nothing
        Set home = ActiveWorkbook
        ScrUpdStatus = Application.ScreenUpdating
    
        Buff = Dir(pBookFullNameToOpen)
        
        If Buff = "" Then
            Err.Raise 800, "OpenBook", "指定したBookはありません"
            Exit Function
        End If
        
        For Each wb In Workbooks
            If wb.Name = Buff Then
                Err.Raise 800, "OpenBook", "指定したBookは既に開いています"
                Exit Function
            End If
        Next wb
        
        Application.ScreenUpdating = False
            Workbooks.Open pBookFullNameToOpen
            Set OpenBook = ActiveWorkbook
            home.Activate
        Application.ScreenUpdating = ScrUpdStatus
        
    End Function

2018年10月20日土曜日

最終行の取得

最終行の取得は、
Cells(Rows.Count, 1).End(xlUp).Row
をしょっちゅうググって(暗記する気がないせい)使うわけですが、サンプルを見るたびにいちいちちょっと気になることが2点あります。
(1) Rows は Range オブジェクトだよね~ と、
(2) なんでサンプルコードに「1」とか列番号を決め打ちするかね、の2点です。

(1)は、シートを移動しないで別のシートのデータ範囲の底の行を取得する場合、例えば、
Worksheets("表紙").Select
With Worksheets("データ")
    BtmRow = .Cells(Rows.Count, 1).End(xlUp).Row
End With
としてしまうと、 Rows.Count は表紙シートの底の行番号を返すんじゃね?それでいいんだっけ?って気になるわけです。
どのシートも底の行数は同じなので特に影響はないですが、気持ち悪いので、.Rows.Count とドットを付けといた方が良いと思います。

(2)は、行を一意にするキーとなる列か、データに空白がない列を指定すればいいわけですが、但し、例えば、表の底に総計行が追加されているような場合は、その行を含めるのか含めないのかによって選択する列が変わるわけです。
なので、表の底に追加されている総計行を含めるのであれば、総計行のラベルの列か総計値の列を指定するので、シートによってケースバイケースなわけです。

サンプルコード


例1のように、シート情報と列番号をパラメータとして渡してどのシートでも使える関数を書いて
最終行番号=最終行("管理表",5)
とか呼び出せば良いわけですが、
'【例1】=================================================================='
    Function 最終行( _
        ByVal pWorksheetName As String, _
        ByVal pColumnNo As Long _
    ) As Long
'========================================================================='
        If pColumnNo <= 0 Then pColumnNo = 1
        With Worksheets(pWorksheetName)
            最終行 = .Cells(.Rows.Count, pColumnNo).End(xlUp).Row
        End With
    End Function
でも、どうせシートによって列構成も違うし、キーとなる列も違うわけです。なので、各シート別に関数を書いて、できれば Date() みたいに定数のように使えると便利だよね~と思うので、例2のようにシート名やデフォルト列を決め打ちして
最終行番号=管理表最終行()
と呼び出せるように、シート別の関数をシートの数だけ書いちゃえば良いと思います。
'【例2】=================================================================='
    Function 管理表最終行( _
        Optional ByVal pColumnNo As Long _
    ) As Long
'========================================================================='
        Const DefaultColumnNo As Long = 5 'デフォルトの列を指定しておく'
        If pColumnNo <= 0 Then pColumnNo = DefaultColumnNo
        With Worksheets("管理表") 'シート名も決め打ち'
            管理表最終行 = .Cells(.Rows.Count, pColumnNo).End(xlUp).Row
        End With
    End Function

2018/12/29:言葉足らずな感じのところを少し手直ししました