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}