001package com.streamconverter.path;
002
003import java.util.ArrayList;
004import java.util.Collections;
005import java.util.List;
006
007/**
008 * 最小限のCSVPath実装
009 *
010 * <p>CSV列選択のパス一致判定のみに特化したシンプルな設計
011 *
012 * <p>複数の列セレクターをOR条件で判定する機能を提供
013 *
014 * <p>セレクターには次を指定できる。
015 *
016 * <ul>
017 *   <li>列名
018 *   <li>0始まりの数値インデックス
019 *   <li>{@code "*"} による全列選択
020 * </ul>
021 *
022 * <p><b>注意:</b> {@code "*"} はワイルドカードとして解釈されるため、ヘッダー名そのものが {@code "*"} の列を個別指定する用途には使えない。
023 */
024public class CSVPath extends AbstractPath<Integer> {
025
026  private final List<String> selectors;
027
028  /**
029   * 単一セレクターでCSVPathを作成
030   *
031   * @param selector 列選択子(列名、数値インデックス、または {@code "*"} による全列選択)
032   * @throws IllegalArgumentException セレクターが不正な場合
033   */
034  private CSVPath(String selector) {
035    super(selector);
036    this.selectors = Collections.singletonList(selector.trim());
037  }
038
039  /**
040   * 複数セレクターでCSVPathを作成(OR条件)
041   *
042   * @param selectorList 列選択子のリスト
043   * @throws IllegalArgumentException セレクターが不正な場合
044   */
045  private CSVPath(List<String> selectorList) {
046    super(String.join(",", selectorList));
047    List<String> temp = new ArrayList<>();
048    for (String sel : selectorList) {
049      temp.add(sel.trim());
050    }
051    this.selectors = Collections.unmodifiableList(temp);
052  }
053
054  /**
055   * 単一セレクターでCSVPathを作成
056   *
057   * @param selector 列選択子(列名、数値インデックス、または {@code "*"} による全列選択)
058   * @return CSVPath instance
059   * @throws IllegalArgumentException セレクターが不正な場合
060   */
061  public static CSVPath of(String selector) {
062    if (selector == null || selector.trim().isEmpty()) {
063      throw new IllegalArgumentException("CSV column selector cannot be null or empty");
064    }
065    return new CSVPath(selector);
066  }
067
068  /**
069   * 複数セレクターでCSVPathを作成(OR条件)
070   *
071   * @param selectorList 列選択子のリスト
072   * @return CSVPath instance
073   * @throws IllegalArgumentException セレクターが不正な場合
074   */
075  public static CSVPath of(List<String> selectorList) {
076    if (selectorList == null || selectorList.isEmpty()) {
077      throw new IllegalArgumentException("Selector list cannot be null or empty");
078    }
079    return new CSVPath(selectorList);
080  }
081
082  /**
083   * 検証・正規化処理のフック。
084   *
085   * <p>{@link AbstractPath#AbstractPath(String)} コンストラクタから呼び出されるが、CSVPathでは検証を ファクトリメソッド({@link
086   * #of(String)} / {@link #of(java.util.List)})側で行うため、
087   * コンストラクタ内スロー(CT_CONSTRUCTOR_THROW)を避けるためにここでは何もしない。
088   *
089   * @param rawSelector 生のセレクター文字列(未使用)
090   */
091  @Override
092  protected void validateAndNormalize(String rawSelector) {
093    // Validation is performed in factory methods (of(...)) to avoid CT_CONSTRUCTOR_THROW
094  }
095
096  /**
097   * 指定された列インデックスがこのパスにマッチするかどうかを判定する(OR条件)。
098   *
099   * <p>いずれかのセレクターがインデックスに一致すれば {@code true} を返す。
100   *
101   * <p><b>注意:</b> このメソッドでインデックス一致として扱えるのは、数値インデックス指定セレクター(例: {@code "0"})と全列選択 {@code "*"} のみ。
102   * 列名指定セレクター (例: {@code "name"}, {@code "userId"})は常に {@code false} を返す。 列名での一致判定には {@link
103   * #matches(String[], int)} を使用すること。
104   *
105   * @param columnIndex 判定対象の列インデックス(0始まり)。nullまたは負値の場合はfalse
106   * @return いずれかのセレクターが一致する場合true
107   */
108  @Override
109  public boolean matches(Integer columnIndex) {
110    if (columnIndex == null || columnIndex < 0) {
111      return false;
112    }
113
114    // OR条件:いずれかのセレクターがマッチすればtrue
115    for (String selector : selectors) {
116      if (matchesSingleSelector(selector, columnIndex)) {
117        return true;
118      }
119    }
120    return false;
121  }
122
123  /**
124   * 列ヘッダー配列との一致判定(OR条件)
125   *
126   * @param headers CSV列ヘッダー配列
127   * @param targetIndex 対象列のインデックス
128   * @return いずれかのセレクターが一致する場合true
129   */
130  public boolean matches(String[] headers, int targetIndex) {
131    if (headers == null || targetIndex < 0 || targetIndex >= headers.length) {
132      return false;
133    }
134
135    // OR条件:いずれかのセレクターがマッチすればtrue
136    for (String selector : selectors) {
137      if (matchesSingleSelector(selector, headers, targetIndex)) {
138        return true;
139      }
140    }
141    return false;
142  }
143
144  /**
145   * マッチするすべての列インデックスを取得(Don't Ask Tell準拠)
146   *
147   * @param headers CSV列ヘッダー配列
148   * @return マッチした列インデックスのリスト
149   */
150  public List<Integer> findMatchingIndices(String[] headers) {
151    List<Integer> matchingIndices = new ArrayList<>();
152    if (headers == null) {
153      return matchingIndices;
154    }
155
156    for (int i = 0; i < headers.length; i++) {
157      if (matches(headers, i)) {
158        matchingIndices.add(i);
159      }
160    }
161    return matchingIndices;
162  }
163
164  /**
165   * マッチするすべての列インデックスを取得(ヘッダーなしの場合)
166   *
167   * @param totalColumns 総列数
168   * @return マッチした列インデックスのリスト
169   */
170  public List<Integer> findMatchingIndices(int totalColumns) {
171    List<Integer> matchingIndices = new ArrayList<>();
172
173    for (int i = 0; i < totalColumns; i++) {
174      if (matches(i)) {
175        matchingIndices.add(i);
176      }
177    }
178    return matchingIndices;
179  }
180
181  /** 単一セレクターの列インデックス一致判定 */
182  private boolean matchesSingleSelector(String selector, Integer columnIndex) {
183    if (isAllColumnsSelector(selector)) {
184      return true;
185    }
186    int parsedIndex = parseAsIndex(selector);
187    if (parsedIndex >= 0) {
188      return parsedIndex == columnIndex;
189    }
190    return false; // 列名指定はヘッダー情報が必要
191  }
192
193  /** 単一セレクターのヘッダー一致判定 */
194  private boolean matchesSingleSelector(String selector, String[] headers, int targetIndex) {
195    if (isAllColumnsSelector(selector)) {
196      return true;
197    }
198    int parsedIndex = parseAsIndex(selector);
199    if (parsedIndex >= 0) {
200      // インデックス指定の場合
201      return parsedIndex == targetIndex;
202    } else {
203      // 列名指定の場合
204      return headers[targetIndex].trim().equalsIgnoreCase(selector.trim());
205    }
206  }
207
208  /** 文字列が数値インデックスかどうかを判定 */
209  private static int parseAsIndex(String selector) {
210    if (selector == null || selector.isEmpty()) {
211      return -1;
212    }
213    try {
214      int index = Integer.parseInt(selector.trim());
215      return index >= 0 ? index : -1;
216    } catch (NumberFormatException e) {
217      return -1;
218    }
219  }
220
221  private static boolean isAllColumnsSelector(String selector) {
222    return "*".equals(selector);
223  }
224
225  /**
226   * このパスの文字列表現を返す。
227   *
228   * <p>セレクターが1つの場合はその値をそのまま返し、複数の場合はカンマ区切りで結合する。
229   *
230   * @return セレクターの文字列表現
231   */
232  @Override
233  public String toString() {
234    if (selectors.size() == 1) {
235      return selectors.get(0);
236    } else {
237      return String.join(",", selectors);
238    }
239  }
240}