改行を含まないデータCSVファイルの読込み

実践で良く処理するCSVは、改行をデータに含まないケースがほとんどである。
改行をデータに含むケースを考慮したものとして、
GitHub - yipuran/yipuran-csv: Java CSV read and write
を作ったが、改行をデータに含まないケースはこれを使わずにもっと少ないコードで
機能を果たせる。
先日投稿した、CSV形式の1行分の文字列からList<String>への変換をinterface method にする。 - Oboe吹きプログラマの黙示録
この Csvtolist を使えば以下のように普遍的に書くこともできる。

/**
 * CSVファイル読込をBiConsumer で処理する
 * @param bifunc 行番号、CSVヘッダで定義するKeyのMap<String, String> の BiConsumer
 */
public static void executeCsvline(BiConsumer<Integer, Map<String, String>> bifunc) throws Exception{
    // getInputStream() は、CSVファイル入力 stream を取得する手段、 
    // AWS S3 などは、代わりに S3ObjectInputStream を返すようなメソッドを用意するなど
    // 的確な入力streamを使用する。
    try(InputStream inst = getMyInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(inst))){
        String line = BOMfunction.chop(reader.readLine());
        AtomicInteger ix = new AtomicInteger(0);
        List<String> hlist = Csvtolist.create(line);
        int rownum = hlist.size();
        Map<Integer, String> hmap =    hlist.stream()
        .collect(()->new HashMap<Integer, String>(),(r, t)->{
            r.put(ix.incrementAndGet(), t);
        },(r, t)->{});
        // TODO ヘッダ行のチェック仕様のチェックはここで⇒ hmap の妥当性、異常あれば RuntimeException で throw
        int lc = 1;
        while((line = reader.readLine()) != null){
            Map<String, String> map = new HashMap<>();
            List<String> list = Csvtolist.create(line);
            if (rownum != list.size()) {
                // ヘッダ行の列数とデータ行の列数が合わない時
                throw new RuntimeException("header rows No match!");
            }
            hmap.entrySet().stream().forEach(e->{
                map.put(e.getValue(), list.get(e.getKey()-1));
            });
            // TODO データ行のチェック仕様のチェックはここで⇒ 異常あれば RuntimeException で throw
            bifunc.accept(lc, map);
            lc++;
        }
    }catch(IOException e){
        throw e;
    }
}

BOM付きなら除去する BOMfunction.chopBOMfunction は、以下である。
yipuran-csv の中にあるが、同じものをここに書いておく)

import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
 * BOM操作ユーティリティクラス.
 */
public final class BOMfunction{
    /** private constructor. */
    private BOMfunction(){}

    /**
     * BOMを出力する.
     * @param out OutputStream
     * @throws IOException
     */
    public static void push(OutputStream out) throws IOException{
        out.write(new byte[]{ (byte)0xef,(byte)0xbb, (byte)0xbf });
    }

    /**
     * BOMが先頭に付いた文字列のBOMを除去する.
     * @param str 除去前の文字列
     * @return 除去後の文字列
     */
    public static String chop(String str){
        byte[] b = str.getBytes();
        if (b.length < 3) return str;
        if (b[0] == -17 && b[1] == -69 && b[2] == -65) {
            byte[] n = new byte[b.length-3];
            for(int i=0,k=3; i < n.length;i++, k++){
                n[i] = b[k];
            }
            return new String(n);
        }else if(b[0] == -2 && b[1] == -1){
            byte[] n = new byte[b.length-2];
            for(int i=0,k=2; i < n.length;i++, k++){
                n[i] = b[k];
            }
            return new String(n);
        }else if(b[0] == -1 && b[1] == -2){
            byte[] n = new byte[b.length-2];
            for(int i=0,k=2; i < n.length;i++, k++){
                n[i] = b[k];
            }
            return new String(n);
        }
        return str;
    }
    /**
     * 文字列がBOM付き文字であるか返す
     * @param str 文字列
     * @return true=BOM付き
     */
    public static boolean match(String str){
        if (str==null) return false;
        byte[] b = str.getBytes();
        if (b.length < 3) return false;
        if (b[0] == -17 && b[1] == -69 && b[2] == -65) return true;
        // UTF_16BE BOM
        if (b[0] == -2 && b[1] == -1) return true;
        // UTF_16LE BOM
        if (b[0] == -1 && b[1] == -2) return true;
        return false;
    }
    /**
     * BOM付き 状況から Charset を返す。
     * @param str 文字列
     * @return java.nio.charset.Charset
     */
    public static Charset getCharset(String str){
        if (str==null) return StandardCharsets.UTF_8;
        byte[] b = str.getBytes();
        if (b.length < 3) return StandardCharsets.UTF_8;
        if (b[0] == -17 && b[1] == -69 && b[2] == -65) return StandardCharsets.UTF_8;
        // UTF_16BE BOM
        if (b[0] == -2 && b[1] == -1) return StandardCharsets.UTF_16BE;
        // UTF_16LE BOM
        if (b[0] == -1 && b[1] == -2) return StandardCharsets.UTF_16LE;
        return StandardCharsets.UTF_8;
    }
}

使用サンプルは、、

executeCsvline(n, m)->{
    // n = データ行番号
    // m = ヘッダ列が示すkeyによる分割結果のMap<String, String>
});

これは、ファイル入力ストリーム等、実装の状況に合わせるサンプルでしかない。
汎用的なものが欲しい。
ファイル入力を組み込んだ interface class として以下を用意する
CsvReadProcess

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;

public interface CsvReadProcess{
    public void inspectLine(int lineno, List<String> list) throws FingersplitException;
    public Charset getCharset();
    
    public static CsvReadProcess of(Charset charset, 
                                   BiFunction<Integer, List<String>, String> inspector) {
        return new CsvReadProcess() {
            @Override
            public void inspectLine(int lineno, List<String> list) throws FingersplitException {
                String res = inspector.apply(lineno, list);
                if (res != null && !res.isBlank()){
                    throw new FingersplitException(lineno, res);
                }
            }
            @Override
            public Charset getCharset(){
                return charset;
            }
        };
    }

    public default void execute(ThrowableSupplier<InputStream> getStreamer, 
                                BiConsumer<Integer, Map<String, String>> bifunc) throws Exception {
        try(InputStream inst = getStreamer.get();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inst, getCharset()))){
            String line = BOMfunction.chop(reader.readLine());
            AtomicInteger ix = new AtomicInteger(0);
            List<String> hlist = Csvtolist.create(line);
            inspectLine(1, hlist);
            int rownum = hlist.size();
            Map<Integer, String> hmap = hlist.stream()
            .collect(()->new HashMap<Integer, String>(),(r, t)->{
                r.put(ix.incrementAndGet(), t);
            },(r, t)->{});
            ix.set(1);
            int lc = 1;
            while((line = reader.readLine()) != null){
                Map<String, String> map = new HashMap<>();
                List<String> list = Csvtolist.create(line);
                if (rownum != list.size()) {
                    throw new FingersplitException(lc, "header rows No match!");
                }
                inspectLine(ix.incrementAndGet(), list);
                hmap.entrySet().stream().forEach(e->{
                    map.put(e.getValue(), list.get(e.getKey()-1));
                });
                bifunc.accept(lc, map);
                lc++;
            }
        }catch(Exception e){
            throw e;
        }
    }
}

この中で使用しているものは以下、

ThrowableSupplier は、入力ストリームを生成して提供することを
約束する Throwableな、Function の Supplier
yipuran-core に、これはあるが、同じものをここに書いておく)
java.util.function.Supplier で例外捕捉をすることで呼び出しの外に例外捕捉を書けるようになる。

import java.io.Serializable;
import java.util.function.Function;
import java.util.function.Supplier;
/**
 * Exception 捕捉 Supplier.
 */
@FunctionalInterface
public interface ThrowableSupplier<R> extends Serializable{
    R get() throws Exception;

    /**
     * ThrowableSupplier 生成.
     * @param supplier 例外スローする Supplier&lt;R&gt;処理
     * @param onCatch Exception捕捉処理 , 値を返さなければならない。
     * @return Supplier&lt;R&gt;
     */
    public static <R> Supplier<R> to(ThrowableSupplier<? extends R> supplier, 
                                    Function<Exception, R> onCatch){
        return ()->{
            try{
                return supplier.get();
            }catch(Exception e){
                return onCatch.apply(e);
            }
        };
    }
    /**
     * ThrowableSupplier 生成(外に例外スロー).
     * @param supplier 例外スローする Supplier処理
     * @return Supplier&lt;R&gt;
     */
    public static <R> Supplier<R> to(ThrowableSupplier<? extends R> supplier){
        return ()->{
            try{
                return supplier.get();
            }catch(Throwable ex){
                throw new RuntimeException(ex.getMessage(), ex);
            }
        };
    }
}

例外を定義:FingersplitException

public class FingersplitException extends RuntimeException{
    private final int lineno;
    private final String message;
    
    public FingersplitException(int lineno, String message) {
        this.lineno = lineno;
        this.message = message;
    }
    public int getLine() {
        return lineno;
    }
    public String getMessage() {
        return message;
    }
    @Override
    public String toString() {
        return "line " + lineno + ": " + message;
    }
}

BOMfunction.chop は、先に書いたとおり。

CsvReadProcess 使用例は、
以下、getCustomInputStream() というCSVファイル
の入力ストリームを取得するメソッドが存在するものとして、

try{
    CsvReadProcess process = CsvReadProcess.of(Charset.forName("MS932"), (i, list)->{
          // TODO  異常があれば、エラーメッセージを返す、正常なら、null またはブランク、空白を返す。
          return  null;
    });
    process.execute(()->getCustomInputStream(), (i, m)->{
        // i = データ行番号
        // m = ヘッダ列が示すkeyによる分割結果のMap<String, String>
    });
}catch(FingersplitException e){
    e.printStackTrace();
}catch(Exception e){
    e.printStackTrace();
}

CSVファイルがUTF-8 なら、Charset.forName("MS932") の代わりに、
StandardCharsets.UTF_8 を使う。