以下の内容はhttps://www.limecode.jp/entry/fungo/053-handle-table-and-age-calculationより取得しました。


53本目:テーブルの扱いと年齢計算

Excel&VBA解説サイト「エクセルの神髄」様出題の問題集、
VBA100本ノック」に対する私の回答と解説のページです。

100本ノックの出題リストはこちらから
excel-ubara.com

出題:テーブルの扱いと年齢計算

#VBA100本ノック 53本目
画像のようにA1から始まるテーブルがあります。
「東京都」の「男」で「35歳以上」の人のみ、備考欄に「対象」と入れてください。
年齢はマクロ実行年(つまり今年)の12月31日で計算。
※シートは任意

テーブルのサンプル

◇ 出題ページはこちら

ソースコード

定義モジュール

Option Explicit

' データ
Public Const R1stデータ = 2
Public Const C1stデータ = 1
Public Enum CNoデータ
    名前 = C1stデータ
    性別
    誕生日
    都道府県
    備考
End Enum
Public Const CLastデータ = CNoデータ.備考

メインモジュール

Option Explicit

' 100本ノック053:テーブルの扱いと年齢計算
Sub 対象者を抽出して備考欄に出力する()
    
    With WSデータ
        Dim R As Long
        For R = .ListObjects(1).DataBodyRange.Row To Get最終行(.ListObjects(1))
            
            If .Cells(R, CNoデータ.性別) = "男" And _
               .Cells(R, CNoデータ.都道府県) = "東京都" And _
               Get暦年年齢(.Cells(R, CNoデータ.誕生日), Year(Date)) >= 35 Then
               
                .Cells(R, CNoデータ.備考) = "対象"
            Else
                .Cells(R, CNoデータ.備考) = ""
            End If
            
        Next
    End With

End Sub

汎用関数モジュール

' 年齢の計算
' 参考:https://www.limecode.jp/entry/utility/datedif-calculate-age
Function Get年齢(ByVal 生年月日 As Date, ByVal 基準日 As Date) As Long
    
    Dim mmdd生年月日 As Long: mmdd生年月日 = Month(生年月日) * 100 + Day(生年月日)
    Dim mmdd基準日 As Long: mmdd基準日 = Month(基準日) * 100 + Day(基準日)
    
    If mmdd基準日 >= mmdd生年月日 Then
        Get年齢 = Year(基準日) - Year(生年月日)
    Else
        Get年齢 = Year(基準日) - Year(生年月日) - 1
    End If
    
End Function

Function Get年度年齢(ByVal 生年月日 As Date, ByVal 年度 As Long) As Long
    Get年度年齢 = Get年齢(生年月日, DateSerial(年度 + 1, 3, 31))
End Function

Function Get学年年齢(ByVal 生年月日 As Date, ByVal 年度 As Long) As Long
    Get学年年齢 = Get年齢(生年月日, DateSerial(年度 + 1, 4, 1))
End Function

Function Get暦年年齢(ByVal 生年月日 As Date, ByValAs Long) As Long
    Get暦年年齢 = Get年齢(生年月日, DateSerial(, 12, 31))
End Function

' 最終行の取得
' 参考:https://www.limecode.jp/entry/library/get-lastrow-lastcolumn
Function Get最終行(指定オブジェクト As Variant, Optional ByVal C As Long = -1) As Long

    ' 渡されたオブジェクトからセル範囲を取得
    Dim 対象セル範囲 As Range
    Select Case TypeName(指定オブジェクト)
    Case "Range"
        If 指定オブジェクト.Cells.CountLarge = 1 Then ' 単独セルにはCurrentRegionを取る
            Set 対象セル範囲 = 指定オブジェクト.CurrentRegion
        Else
            Set 対象セル範囲 = 指定オブジェクト
        End If
    Case "Worksheet"
        Set 対象セル範囲 = 指定オブジェクト.UsedRange
    Case "AutoFilter", "ListObject"
        Set 対象セル範囲 = 指定オブジェクト.Range
    Case Else
        Err.Raise 1000, , "対象外のオブジェクト「" & TypeName(指定オブジェクト) & "」が指定されました。"
    End Select

    ' エリアの最終行を取得
    Get最終行 = 対象セル範囲.Rows.Count + 対象セル範囲.Row - 1

    ' 列が指定されていればその列の入力最終行を取得
    If C <> -1 Then
        Do While 対象セル範囲.Worksheet.Cells(Get最終行, C) = ""
            Get最終行 = Get最終行 - 1
            If Get最終行 < 対象セル範囲.Row Then
                Get最終行 = 0
                Exit Function
            End If
        Loop
    End If

End Function

解説

テーブルの扱いと年齢の計算を問う問題でした。

まずは「テーブルであることは意識せず純粋な行列指定」で解答しています。


年齢の計算はシート上で行えるのであればDATEDIF関数が使えますが、
VBA上で行う場合は何らかの方法を考えなければいけません。
※ VBAのDateDiff関数は年齢の計算には使えない。

詳細はこちらの記事をご覧ください。


本サイトは「汎用関数づくりと使い方」をテーマにノックを解答していますので、
年齢計算も汎用関数にして対応しています。

使用していませんが「Get年度年齢」「Get学年年齢」の関数も置いておきました。


実務では「3/31時点の年齢」の方が良く出てきてしかも面倒ですので、
しっかり汎用関数にして持っておくとよいでしょう。


ただ、実務の話をするのであれば、
そもそもテーブル内に「年齢」列を絶対に作るべきです。

(もちろんこれは問題用のテーブルなので出題がおかしいという意味ではない)


列を増やすだけでマクロが簡単になるのであれば、
そちらに手を入れるスタンスはしっかり持っておいてください。

テーブル内のセルをループする方法

テーブル内のセルを指定する方法はたくさんあり、
それぞれに一長一短の特徴があります。

並べてみますので参考にしてみてください。

DataBodyRange.Rowを活用

Sub 対象者を抽出して備考欄に出力する_DataBodyRange利用()

    Dim テーブル各行 As Range
    For Each テーブル各行 In WSデータ.ListObjects(1).DataBodyRange.Rows
        
        If テーブル各行.Cells(CNoデータ.性別) = "男" And _
           テーブル各行.Cells(CNoデータ.都道府県) = "東京都" And _
           Get暦年年齢(テーブル各行.Cells(CNoデータ.誕生日), Year(Date)) >= 35 Then
           
            テーブル各行.Cells(CNoデータ.備考) = "対象"
        Else
            テーブル各行.Cells(CNoデータ.備考) = ""
        End If
        
    Next

End Sub

 
DataBodyRange.RowsをFor Eachでループすることで、
「行番号」を意識しなくてよくなったコードです。

テーブルが上下に移動してもコードを書き替えなくてよいですが、
テーブルがA列始まりでない場合はEnumの調整が必要な点にご注意ください。


最終行の取得をしなくてもよくなることもメリットなのですが、
「Get最終行」の関数を用意すればそんなに差はありません。

ListRowオブジェクトを活用

Sub 対象者を抽出して備考欄に出力する_ListRow利用()

    Dim テーブル各行 As ListRow
    For Each テーブル各行 In WSデータ.ListObjects(1).ListRows
        
        If テーブル各行.Range(CNoデータ.性別) = "男" And _
           テーブル各行.Range(CNoデータ.都道府県) = "東京都" And _
           Get暦年年齢(テーブル各行.Range(CNoデータ.誕生日), Year(Date)) >= 35 Then
           
            テーブル各行.Range(CNoデータ.備考) = "対象"
        Else
            テーブル各行.Range(CNoデータ.備考) = ""
        End If
        
    Next

End Sub

 
ListRowsをFor Eachでループすることで、
DataBodyRange.Rowsと同様「行番号」を意識しなくてよくなったコードです。


最初からRangeオブジェクトを取得できるのがDataBodyRange.Rowsの良さで、
ListRowオブジェクトはそのあと.Rangeを挟む必要があります。

ListRow専用メソッドで使えるものはDeleteくらいしかありませんので、
DataBodyRange.Rowsとほとんど同じコードだと思ってください。

ListColumns.Index(列番号取得)を活用

Sub 対象者を抽出して備考欄に出力する_ListColumnsIndex利用()

    With WSデータ.ListObjects(1)
        Dim テーブル各行 As ListRow
        For Each テーブル各行 In .ListRows
            
            If テーブル各行.Range(.ListColumns("性別").Index) = "男" And _
               テーブル各行.Range(.ListColumns("都道府県").Index) = "東京都" And _
               Get暦年年齢(テーブル各行.Range(.ListColumns("誕生日").Index), Year(Date)) >= 35 Then
               
                テーブル各行.Range(.ListColumns("備考").Index) = "対象"
            Else
                テーブル各行.Range(.ListColumns("備考").Index) = ""
            End If
            
        Next

    End With

End Sub

 
列番号を「.ListColumns("列名").Index」で取得するパターンです。

ここまで組むとテーブルが上下左右に移動してもコード書き換え不要になりますが、
反面、列名変更で動かなくなるのがデメリットですね。

テーブルの性質を余すことなく利用したコードと言えるでしょう。


このコードは、

If WSデータ.Cells(R, 2) = "男" Then

という列番号ベタ打ちコードに比べると画期的に感じますが、
 

If WSデータ.Cells(R, CNoデータ.性別) = "男" Then

というEnum活用コードを使っている方にとっては、一長一短という気がします。


Enumは定義する手間がありますが、
そのあとは選択肢から入力ができるという強力なメリットがありますからね。

テーブルが動いてもコード改修不要というのは確かにメリットですが、
Enumも定義部分の調整だけで済むため、大きく差がある部分ではありません。


状況に応じて使い分けてみてください。

ListColumnオブジェクトを活用

Sub 対象者を抽出して備考欄に出力する_ListColumnオブジェクト利用()

    With WSデータ.ListObjects(1)
        Dim i As Long
        For i = 1 To .DataBodyRange.Rows.Count
            
            If .ListColumns("性別").DataBodyRange(i) = "男" And _
               .ListColumns("都道府県").DataBodyRange(i) = "東京都" And _
               Get暦年年齢(.ListColumns("誕生日").DataBodyRange(i), Year(Date)) >= 35 Then
               
                .ListColumns("備考").DataBodyRange(i) = "対象"
            Else
                .ListColumns("備考").DataBodyRange(i) = ""
            End If
            
        Next

    End With

End Sub

 
最後にListColumnをそのままオブジェクトとして活用したパターンです。

今まではテーブルを横(行ごと)にスライスして扱うイメージのコードでしたが、
こちらはテーブルを縦(列ごと)に割ったRangeを使うイメージですね。


何か明確なメリットがあるわけではなく、
処理のイメージがしづらいのでわざわざ使う必要はないかもしれません。


この書き方だと明確に書きやすいような処理があれば使ってみてください。




以上の内容はhttps://www.limecode.jp/entry/fungo/053-handle-table-and-age-calculationより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14