Option Explicit

'===========================================================
' 部品_MATCH検索位置取得 / GetMatchPosition
'   ・指定した範囲（1 列）から、検索値と一致する最初のセルの「位置」を返す（完全一致）
'     → Returns the position of the first cell equal to the search value in the specified one-column range
'        (exact match).
'
'   ・位置は、範囲の最初の行を 1 とした番号（Excel の MATCH 関数と同じ）。見つからない場合は 0 を返す
'     → The position counts the first row of the range as 1 (same as Excel's MATCH function);
'        returns 0 if not found.
'
'   ※ この部品は、見つからない場合も、エラーでは止まらず、0 を返す（見つからないのは、異常ではなく、結果のひとつ）
'     → ※ Not found is a normal result: the component returns 0 instead of raising an error.
'===========================================================

'===========================================================
' ■区分 / Classification
'   ・基礎区分：土台 ／ 用途区分：取得 ／ 操作対象区分：範囲
'     → Base: foundation / Purpose: get / Target: range
'
' ■バージョン / Version
'   ・Version 1.0（2026-10-29）
'===========================================================

'===========================================================
' ■URL（WordPress部品ページ）
'   https://feelcodelab.com/vba_parts/getmatchposition/
'===========================================================

'===========================================================
' ■前提条件（AI誤動作防止の最重要項目） / Preconditions
'   ・【最重要】検索する範囲（vSearchRng）は、呼び出し側が、範囲設定の部品で作って、変数に入れて渡す
'     → 範囲は、1 列だけの範囲にする（複数列の場合は、エラーで止まる）
'     → 最終行が可変の場合は、範囲設定_行可変で作る（見つからない場合は Nothing になるので、先に確認する）
'     → [MOST IMPORTANT] The caller builds the search range with a range-setting component and passes the
'        variable. It must be a one-column range.
'
'   ・【最重要】戻り値は「位置」であり、シート上の行番号ではない
'     → 範囲の最初の行を 1 とした番号。シート上の行番号は、次の式で求める
'         シート上の行番号 ＝ vSearchRng.Row ＋ 戻り値 − 1
'     → [MOST IMPORTANT] The return value is a position counted from the first row of the range, not a sheet
'        row number. Sheet row = vSearchRng.Row + return value - 1.
'
'   ・【最重要】戻り値が 0 の場合は「見つからなかった」ことを表す。呼び出し側で必ず確認してから使う
'     → 0 のまま、シート上の行番号を計算すると、誤った行（範囲の 1 つ上）を指す
'     → [MOST IMPORTANT] 0 means "not found". The caller must check it before using the value.
'
'   ・検索は「完全一致」で、英字の大文字・小文字は区別する（"a-001" と "A-001" は別の値）
'     ※ Excel の MATCH 関数は区別しないが、この部品は、コードや ID の取り違えを防ぐため、区別する
'     → Exact match; uppercase/lowercase ARE distinguished (unlike Excel's MATCH), to avoid mixing up codes and IDs.
'
'   ・文字列の「1」と、数値の 1 は、一致しない（Excel の MATCH 関数と同じ）
'     数値と日付は、値が同じなら一致する。セルのエラー値（#N/A など）は、検索の対象外
'     → The text "1" and the number 1 do not match (same as Excel's MATCH). Cells with error values are skipped.
'
'   ・同じ値が複数ある場合は、最初に見つかった（最も上の）位置を返す
'     → If the value appears more than once, the first (top-most) position is returned.
'
'   ・検索値が空（Empty・空文字・Null・エラー値）の場合は、呼び出し側の誤りとして、エラーで止まる
'     → An empty search value (Empty, "", Null, error value) stops with an error as a caller mistake.
'
'   ・設定の誤りは、0 ではなく、原因を示すエラーで止まる（Err.Raise）
'       - 検索範囲（vSearchRng）が Nothing（vbObjectError + 1014）
'       - 検索範囲が、1 列でない（vbObjectError + 1017）
'       - 検索値が空（vbObjectError + 1016）
'     → Setting mistakes stop with an error naming the cause instead of returning 0.
'
'   ・Offset / Find / Resize / With は禁止（検索は、配列に取り込んで比較する）
'     → Offset, Find, Resize, and With statements are prohibited (values are loaded into an array and compared).
'===========================================================

'===========================================================
' ■依存関係（部品名明記） / Component Dependencies
'   ・他の部品を参照しない（土台）
'     → References no other component (foundation).
'
'   ・組み合わせ：範囲設定_固定（または範囲設定_行可変）→ MATCH検索位置取得
'     （呼び出し側が、範囲を作って変数に入れ、その変数を渡す）
'     → Typical combination: [SetRange_Fixed / SetRange_VariableRows] -> [GetMatchPosition]
'
'   ・組み合わせ：MATCH検索位置取得 → 範囲設定_固定 など
'     （見つけた行番号を使って、その行の範囲を作る）
'     → Typical combination: [GetMatchPosition] -> [SetRange_Fixed]
'===========================================================

'===========================================================
' ■依存関係（シート・表） / Sheet Dependencies
'   ・特定のシートや表の名前には、依存しない（呼び出し側が渡した範囲だけを使う）
'     → Does not depend on any specific sheet or table name; it uses only the range passed by the caller.
'
'   ・検索する範囲（vSearchRng）は、検索する値が縦に並んだ、1 列の範囲にする
'     → The search range (vSearchRng) must be a single column holding the values to search.
'
'   ・戻り値は、範囲の最初の行を 1 とした「位置」（シート上の行番号は、範囲の先頭行を使って、呼び出し側で求める）
'     → The result is a position counted from the first row of the range; the caller derives the sheet row from the top row of the range.
'===========================================================

'===========================================================
' ■技術解説 / Technical Notes
'
'   ・範囲（Range）で受け取る理由
'       → 範囲を操作する部品は、範囲消去・範囲コピーと同じく、Range で受け取る。範囲は、自分のブック・シートを
'          持っているため、ブックやシート名の引数が不要で、部品が他の部品（範囲設定）を参照しなくて済む。
'       → Like the clear/copy components, a range-operating component receives a Range; it carries its own
'          workbook and sheet, so no other component needs to be referenced.
'
'   ・戻り値を、行番号ではなく「位置」にする理由
'       → Excel の MATCH 関数と同じ意味にすることで、AI も人も、迷わずに使える。
'          また、検索範囲の開始行が変わっても、戻り値の意味が変わらない。
'       → Same meaning as Excel's MATCH, so AI and people use it without confusion.
'
'   ・見つからない場合に、エラーではなく、0 を返す理由
'       → 見つからないことは、実行する前から、起こりうる結果のひとつで、原因もはっきりしている（一致する値が無い）。
'          呼び出し側が、戻り値で、「見つからない場合」の処理を選べるようにしている。
'       → Not found is an expected result with a clear cause, so the caller decides what to do via the return value.
'
'   ・大文字・小文字を区別する理由
'       → 商品コードや ID は、大文字・小文字が違えば別の値であることが多い。区別しないと、別のデータを見つけてしまい、
'          気づきにくい誤りになる。区別して、見つからない（0）になる方が、誤りに気づきやすい。
'          区別しない検索が必要な場合は、別の部品にする（迷ったら分ける）。
'       → Codes/IDs that differ in case are usually different values; not matching (0) is easier to notice.
'
'   ・Find メソッドを使わず、配列に取り込んで比較する理由
'       → Find は、前回の検索条件が残るなど、挙動が予測しにくい。配列での比較は、結果が一定で、
'          1 セルずつ Value を読むより、高速に動作する。
'       → Find keeps previous search settings and is unpredictable; comparing an array gives a constant result
'          and is faster than reading cells one by one.
'
'   ・On Error を使わない理由
'       → 設定の誤りは、実行の前に確認してエラーで止める。検索の途中で、実行時に失敗する処理はない。
'       → Setting mistakes are checked beforehand, and nothing here fails unpredictably at run time.
'===========================================================

'---------------------------------------------
' ■引数（型＋意味） / Arguments
'   vSearchRng   : 検索する範囲（Range）。1 列だけの範囲。範囲設定の部品で作る
'                  → Range to search (Range); one column only, made by a range-setting component.
'
'   vSearchValue : 検索値（Variant）。文字列・数値・日付
'                  → Search value (Variant): text, number, or date.
'---------------------------------------------

'---------------------------------------------
' ■戻り値（型＋意味） / Return Value
'   Long : 見つかった場合は、範囲の最初の行を 1 とした位置。見つからない場合は 0
'          → The position (first row of the range = 1) if found, 0 if not found.
'---------------------------------------------

'---------------------------------------------
' ■エラー / Errors (Err.Raise)
'   ・vbObjectError + 1014：検索範囲（vSearchRng）が Nothing
'   ・vbObjectError + 1016：検索値が空（Empty・空文字・Null・エラー値）
'   ・vbObjectError + 1017：検索範囲が、1 列でない
'---------------------------------------------

'===========================================================
' ■禁止事項（AI向け） / Prohibitions
'   ・戻り値を、シート上の行番号として、そのまま使うことの禁止
'     （シート上の行番号 ＝ vSearchRng.Row ＋ 戻り値 − 1）
'     → Do not use the return value as a sheet row number as it is.
'
'   ・戻り値が 0（見つからない）の場合を、確認せずに使うことの禁止
'     → Do not use the value without checking for 0 (not found).
'
'   ・検索範囲を、複数列で渡すことの禁止（1 列だけにする）
'     → Do not pass a multi-column range.
'
'   ・セル参照の直書き禁止（Range("A1") など）
'     → Hard-coded cell references are prohibited.
'
'   ・Offset禁止 / Find禁止 / Resize禁止 / With禁止
'     → Offset, Find, Resize, and With blocks are prohibited.
'===========================================================

'===========================================================
' ■使用例（サンプルコード） / Example
'   ◆例1：1 列目で検索し、見つかった行の 3 列目の値を取得する / Example 1◆
'   Dim lastRow As Long
'   Dim rng     As Range
'   Dim pos     As Long
'   Dim dataRow As Long
'
'   lastRow = 部品_最終行取得(ThisWorkbook, "顧客表", 1)                      '1列目の最終行
'   Set rng = 部品_範囲設定_固定(ThisWorkbook, "顧客表", 2, 1, lastRow, 1)    '2行目〜最終行の、1列目
'   pos = 部品_MATCH検索位置取得(rng, "A-001")
'   If pos = 0 Then
'       MsgBox "A-001 は、見つかりませんでした"
'       Exit Sub
'   End If
'
'   dataRow = rng.Row + pos - 1                                'シート上の行番号 ＝ 範囲の開始行 ＋ 位置 − 1
'   MsgBox ThisWorkbook.Worksheets("顧客表").Cells(dataRow, 3).Value
'
'   ※ 設定の誤り（Nothing、複数列、検索値が空）の場合は、0 ではなく、
'      原因を示すエラーで止まる（起点で On Error で受けて、Err.Description を表示する）
'===========================================================

'===========================================================
' ■ラッパー関数（日本語名） / Wrapper Function
'   ・辞書の日本語名で呼び出すための窓口
'     → Entry point callable by the Japanese dictionary name.
'
'   ・内部では英語名の本体処理を呼び出し、結果（Long）をそのまま返す
'     → Internally calls the English-named core function and returns its result (Long) as it is.
'===========================================================
Function 部品_MATCH検索位置取得( _
        ByVal vSearchRng As Range, _
        ByVal vSearchValue As Variant) As Long

    '本体処理（英語名）を呼び出し、結果をそのまま返す / Call core function and return its result
    部品_MATCH検索位置取得 = f_GetMatchPosition(vSearchRng, vSearchValue)

End Function

'===========================================================
' ■本体処理（英語名） / Core Function
'   ・指定した範囲から、検索値と一致する最初のセルの位置を返す（見つからない場合は 0）
'     → Returns the position of the first cell equal to the search value (0 if not found).
'===========================================================
Function f_GetMatchPosition( _
        ByVal vSearchRng As Range, _
        ByVal vSearchValue As Variant) As Long

    Dim arr As Variant
    Dim cellValue As Variant
    Dim rowCount As Long
    Dim i As Long
    Dim isSearchText As Boolean

    f_GetMatchPosition = 0

    '---------------------------------------------
    ' ■範囲・検索値を確認（誤りの場合はエラーで止める）
    ' / Check the range and the search value (stop with an error if wrong)
    '---------------------------------------------
    If vSearchRng Is Nothing Then
        Err.Raise vbObjectError + 1014, "f_GetMatchPosition", _
                  "検索範囲（vSearchRng）が設定されていません（Nothing）。"
    End If

    If vSearchRng.Columns.Count <> 1 Then
        Err.Raise vbObjectError + 1017, "f_GetMatchPosition", _
                  "検索範囲（vSearchRng）は、1 列だけの範囲にしてください（現在は " & _
                  vSearchRng.Columns.Count & " 列）。"
    End If

    If IsError(vSearchValue) Then
        Err.Raise vbObjectError + 1016, "f_GetMatchPosition", _
                  "検索値（vSearchValue）が、エラー値です。"
    End If

    If IsNull(vSearchValue) Or IsEmpty(vSearchValue) Then
        Err.Raise vbObjectError + 1016, "f_GetMatchPosition", _
                  "検索値（vSearchValue）が空です（Empty または Null）。"
    End If

    If VarType(vSearchValue) = vbString Then
        If Len(vSearchValue) = 0 Then
            Err.Raise vbObjectError + 1016, "f_GetMatchPosition", _
                      "検索値（vSearchValue）が空です（空文字）。"
        End If
    End If

    '---------------------------------------------
    ' ■値を配列に取り込む（1 セルだけの場合は、配列にならないため、配列を作って入れる）
    '   / Load values into an array (a single cell returns a scalar, so build a 1x1 array)
    '---------------------------------------------
    rowCount = vSearchRng.Rows.Count

    If rowCount = 1 Then
        ReDim arr(1 To 1, 1 To 1)
        arr(1, 1) = vSearchRng.Value
    Else
        arr = vSearchRng.Value
    End If

    isSearchText = (VarType(vSearchValue) = vbString)

    '---------------------------------------------
    ' ■上から順に比較（最初に一致した位置を返す） / Compare from the top (return the first match)
    '---------------------------------------------
    For i = 1 To rowCount

        cellValue = arr(i, 1)

        If IsError(cellValue) = False Then

            If VarType(cellValue) = vbString Then

                '文字列同士：大文字・小文字を区別して比較 / text vs text: case-sensitive
                If isSearchText Then
                    If StrComp(cellValue, vSearchValue, vbBinaryCompare) = 0 Then
                        f_GetMatchPosition = i
                        Exit Function
                    End If
                End If

            ElseIf IsEmpty(cellValue) = False Then

                '数値・日付同士：値が同じなら一致 / number/date: equal if the values are the same
                If isSearchText = False Then
                    If cellValue = vSearchValue Then
                        f_GetMatchPosition = i
                        Exit Function
                    End If
                End If

            End If

        End If

    Next i

End Function

