Excel&VBA解説サイト「エクセルの神髄」様出題の問題集、
「VBA100本ノック」に対する私の回答と解説のページです。
100本ノックの出題リストはこちらから
excel-ubara.com
出題:独自のZLOOKUP関数を作成
#VBA100本ノック 62本目
VLOOKUPは条件合致の先頭が取得されます。
この取得順番を指定できる関数を作成します。
ZLOOKUP(検索値,範囲,列番号,順番)
順番→0:先頭,-1:最後,>=1:その順番
※当該順がない場合は空白""を返す
※検索値がない場合は#N/Aを返す
※完全一致のみ対応、検索値は単一セル値限定


◇ 出題ページはこちら
ソースコード
メインモジュール
Option Explicit ' 100本ノック062:独自のZLOOKUP関数を作成 Function ZLOOKUP(ByVal 検索値 As Variant, 範囲 As Range _ , ByVal 列番号 As Long, 順番 As Long) As Variant ' 列番号の引数チェック Dim 範囲列数 As Long: 範囲列数 = 範囲.Columns.Count Select Case 列番号 Case 0: ZLOOKUP = CVErr(xlErrValue): Exit Function Case Is < -1: ZLOOKUP = CVErr(xlErrValue): Exit Function Case Is > 範囲列数: ZLOOKUP = CVErr(xlErrRef): Exit Function End Select ' 範囲内の列番号をシート上の列番号に変換 列番号 = 列番号 + 範囲.Column - 1 ' 初期値として#N/Aをセット ZLOOKUP = CVErr(xlErrNA) ' 検索範囲の初期値をセット(範囲内の第1列 ∩ UsedRange) Dim ws検索対象 As Worksheet: Set ws検索対象 = 範囲.Worksheet Dim 検索範囲 As Range: Set 検索範囲 = Intersect(範囲, ws検索対象.UsedRange) If 検索範囲 Is Nothing Then Exit Function Set 検索範囲 = 検索範囲.Columns(1) ' Match関数による検索を繰り返す Dim R_発見行 As Long Dim R_前回の発見行 As Long Dim Count発見数 As Long: Count発見数 = 0 Do R_発見行 = Match行番号(検索値, 検索範囲) ' 検索値なし If R_発見行 = 0 Then ' 順番が-1のときは前回の値を返してExit If 順番 = -1 And R_前回の発見行 > 0 Then ZLOOKUP = ws検索対象.Cells(R_前回の発見行, 列番号) Exit Function ' それ以外はExit(1つは見つかっていたらNAを""に上書き) Else If Count発見数 >= 1 Then ZLOOKUP = "" Exit Function End If ' 検索値あり Else Count発見数 = Count発見数 + 1 ' 指定した順番に到達したら結果を返してExit If Count発見数 = 順番 Or 順番 = 0 Then ZLOOKUP = ws検索対象.Cells(R_発見行, 列番号) Exit Function End If End If ' 前回の発見を記録 R_前回の発見行 = R_発見行 ' 検索範囲を発見行以下に狭めて次のループへ Set 検索範囲 = Intersect(検索範囲, ws検索対象.Rows(R_発見行 + 1).Resize(検索範囲.Rows.Count)) Loop End Function
汎用関数モジュール
Option Explicit ' WorksheetFunctionの短縮取得 ' 参考:https://www.limecode.jp/entry/utility/shortcall-worksheetfunction Function Fx() As WorksheetFunction Set Fx = WorksheetFunction End Function ' 行番号の検索 ' ◆ ブログ未掲載 Function Match行番号(ByVal 検索値 As Variant, 検索エリア As Range) As Long On Error Resume Next If IsDate(検索値) Then Dim x: x = CDbl(検索値) If Err.Number = 0 Then Match行番号 = Fx.Match(CDbl(検索値), 検索エリア, 0) + 検索エリア.Row - 1 Exit Function End If End If Match行番号 = Fx.Match(検索値, 検索エリア, 0) + 検索エリア.Row - 1 End Function
解説
発見順を指定できるVLOOKUPを作る問題でした。
ロジックとしては、
- 検索範囲内をMatch関数で検索
- 発見数 = 指定順 に到達したら値を返す
- そうでなければ検索範囲を狭めて次のループへ
という手順で検索を繰り返していきます。
順番を「-1」としたときは後ろから検索を行いますが、
下からひとつづつ見るよりMatch関数を複数回やった方が早いため、
「最後の発見位置」を使って上記のロジック内で一緒にやっています。
問題とは別に割と便利な関数が出来ていると思いますので、
こんな作業を普段行っているならユーザー定義関数に採用してあげてください。