Java Tips | 基本ユーティリティ:TSV対応

Java Java
スポンサーリンク

TSV対応は「CSVの兄弟をちゃんと扱う」技

業務システムでは、CSV だけでなく TSV(Tab Separated Values)もよく出てきます。
「Excel からタブ区切りで出したデータ」「ログをタブ区切りで吐くミドルウェア」など、現場には普通に混在しています。

TSV は「カンマの代わりにタブで区切るだけでしょ?」と思いがちですが、
CSV 用に作ったユーティリティをそのまま流用しようとすると、地味に設計が崩れます。
だからこそ、「区切り文字を差し替えられる設計」にしておくことが、実務での TSV 対応の肝になります。


発想の転換:「CSV専用」ではなく「区切り文字付きテキスト」として扱う

「カンマ固定」のコードはすぐに限界が来る

前回までに作ったような CSV ユーティリティは、だいたいこんな前提で書いていました。

  • 区切り文字は , 固定
  • ダブルクォートで囲む/囲まないのルールは CSV 準拠

このままだと、「TSV も扱いたい」となった瞬間に、
",""\t" に書き換えた別バージョンをコピペで作る、という悲しい未来が見えます。

そこで発想を変えて、「“区切り文字付きテキスト”を扱う汎用ユーティリティ」として設計し直すと、
CSV も TSV も「同じ仕組みの上で、区切り文字だけ変える」だけで対応できるようになります。


区切り文字をパラメータ化した「行生成ユーティリティ」

DelimitedLineBuilder という“土台”を作る

まずは、「区切り文字を外から渡せる行生成ユーティリティ」を作ります。
CSV のときは ,、TSV のときは \t を渡すイメージです。

import java.util.StringJoiner;

public final class DelimitedLineBuilder {

    private DelimitedLineBuilder() {}

    public static String buildLine(char delimiter, String... values) {
        StringJoiner joiner = new StringJoiner(String.valueOf(delimiter));
        for (String v : values) {
            joiner.add(escapeForCsvLike(v, delimiter));
        }
        return joiner.toString();
    }

    private static String escapeForCsvLike(String value, char delimiter) {
        if (value == null) {
            return "";
        }
        boolean needQuote = false;

        if (value.indexOf(delimiter) >= 0 ||
            value.contains("\"") ||
            value.contains("\n") ||
            value.contains("\r")) {
            needQuote = true;
        }

        String escaped = value.replace("\"", "\"\"");

        if (needQuote) {
            return "\"" + escaped + "\"";
        } else {
            return escaped;
        }
    }
}
Java

ここで深掘りしたい重要ポイントは、「“CSV 的なルール”はそのままに、区切り文字だけを変えられるようにしている」ことです。
TSV でも、「タブを含む値」「改行を含む値」「ダブルクォートを含む値」が出てきたら、
CSV と同じようにダブルクォートで囲んであげたほうが安全です。

つまり、「区切り文字は変わるけれど、“囲み方とエスケープのルール”は CSV と同じでよい」という割り切りが、実務ではかなり使えます。


CSV専用・TSV専用の薄いラッパーを用意する

呼び出し側から「区切り文字」を隠す

毎回 buildLine('\t', ...) と書くのは少しノイズなので、CSV/TSV 用の薄いラッパーを用意します。

public final class CsvLines {

    private CsvLines() {}

    public static String build(String... values) {
        return DelimitedLineBuilder.buildLine(',', values);
    }
}
Java
public final class TsvLines {

    private TsvLines() {}

    public static String build(String... values) {
        return DelimitedLineBuilder.buildLine('\t', values);
    }
}
Java

使い方はこうです。

String csv = CsvLines.build("1", "山田,太郎", "営業部");
String tsv = TsvLines.build("1", "山田\t太郎", "営業部");

System.out.println(csv); // 1,"山田,太郎",営業部
System.out.println(tsv); // 1,"山田\t太郎",営業部  (タブを含む場合はダブルクォートで囲まれる)
Java

ここでのポイントは、「呼び出し側は“CSV か TSV か”だけを意識すればよく、区切り文字そのものは見なくていい」ことです。
土台の DelimitedLineBuilder は「区切り文字付きテキストのルール」を知っていて、
CsvLinesTsvLines は「どの区切り文字を使うか」だけを決めています。


区切り文字をパラメータ化した「行分解ユーティリティ」

CsvLineParser を「DelimitedLineParser」に進化させる

次は、CSV 用に作った CsvLineParser を、区切り文字を変えられる形に一般化します。

import java.util.ArrayList;
import java.util.List;

public final class DelimitedLineParser {

    private DelimitedLineParser() {}

    public static List<String> parseLine(char delimiter, String line) {
        List<String> result = new ArrayList<>();
        if (line == null || line.isEmpty()) {
            result.add("");
            return result;
        }

        StringBuilder current = new StringBuilder();
        boolean inQuotes = false;
        int len = line.length();

        for (int i = 0; i < len; i++) {
            char c = line.charAt(i);

            if (c == '"') {
                if (inQuotes && i + 1 < len && line.charAt(i + 1) == '"') {
                    current.append('"');
                    i++;
                } else {
                    inQuotes = !inQuotes;
                }
            } else if (c == delimiter && !inQuotes) {
                result.add(current.toString());
                current.setLength(0);
            } else {
                current.append(c);
            }
        }
        result.add(current.toString());
        return result;
    }
}
Java

そして、CSV/TSV 用の薄いラッパーを用意します。

import java.util.List;

public final class CsvParser {

    private CsvParser() {}

    public static List<String> parse(String line) {
        return DelimitedLineParser.parseLine(',', line);
    }
}
Java
import java.util.List;

public final class TsvParser {

    private TsvParser() {}

    public static List<String> parse(String line) {
        return DelimitedLineParser.parseLine('\t', line);
    }
}
Java

ここで深掘りしたいのは、「“ダブルクォートの扱い”は CSV と TSV で共通にしている」ことです。
TSV でも、値の中にタブや改行が入ることは普通にあります。
そのとき、「ダブルクォートで囲んで、"" でエスケープする」という CSV 的ルールをそのまま使えば、
CSV と TSV の両方を同じ感覚で扱えるようになります。


例題:同じエンティティを CSV と TSV の両方で出力する

出力フォーマットだけ差し替えられる設計にする

例えば、ユーザー一覧を CSV で出すバッチと、TSV で出すバッチが両方必要になったとします。
エンティティは共通で、フォーマットだけ変えたいイメージです。

public class User {
    private final String id;
    private final String name;
    private final String email;

    // コンストラクタや getter は省略
}
Java

CSV 用と TSV 用のマッパーを、それぞれ薄く用意します。

public final class UserCsvMapper {

    private UserCsvMapper() {}

    public static String header() {
        return CsvLines.build("id", "name", "email");
    }

    public static String toLine(User user) {
        return CsvLines.build(
                user.getId(),
                user.getName(),
                user.getEmail()
        );
    }
}
Java
public final class UserTsvMapper {

    private UserTsvMapper() {}

    public static String header() {
        return TsvLines.build("id", "name", "email");
    }

    public static String toLine(User user) {
        return TsvLines.build(
                user.getId(),
                user.getName(),
                user.getEmail()
        );
    }
}
Java

ここでの重要ポイントは、「“列の並びや項目名”は CSV/TSV で共通だが、“区切り文字とエスケープの仕方”だけを差し替えている」ことです。
エンティティから「どの順番で何を出すか」は共通の設計で、
出力フォーマットだけを変えたいときに、マッパーの呼び分けだけで対応できます。


TSV対応で意識したい「CSVとの違い」と「共通点」

違い:人間が見るときの読みやすさ

TSV は、タブ区切りなので、テキストエディタで開いたときに「カラムが揃って見える」ことが多いです。
ログや一時的なデバッグ出力では、「TSV のほうが目で追いやすい」という理由で選ばれることもあります。

一方で、カンマよりも目に見えにくいので、「どこが区切りか分かりづらい」という側面もあります。
その意味でも、「CSV と TSV を同じユーティリティで扱えるようにしておく」と、
「人間が見たいときは TSV」「他システム連携では CSV」といった使い分けがしやすくなります。

共通点:ダブルクォートのルールはそのまま使える

実務で扱う TSV の多くは、「区切り文字がタブなだけで、ダブルクォートのルールは CSV と同じ」で問題ありません。
つまり、「タブや改行を含む値はダブルクォートで囲む」「""" にする」というルールを共通化できます。

ここを共通化しておくと、「CSV も TSV も“同じ世界のフォーマット”」として扱えるようになり、
ユーティリティの再利用性が一気に上がります。


まとめ:TSV対応ユーティリティで身につけたい感覚

TSV対応は、「CSV 用に書いたコードをコピペで改造する」ことではなく、
「最初から“区切り文字付きテキスト”として設計し、区切りだけ差し替えられるようにする」ための考え方です。

押さえておきたい感覚は、まず「行生成・行分解の土台を DelimitedLineBuilder/DelimitedLineParser のように一般化する」こと。
次に、「CSV 用・TSV 用の薄いラッパーを用意して、呼び出し側から区切り文字の存在を隠す」こと。
そして、「ダブルクォートのルールは CSV/TSV で共通にして、生成と分解をペアで設計する」ことです。

タイトルとURLをコピーしました