souki-paranoiastのブログ

地方都市でプログラマーをやっている人のブログ。技術ネタ以外も少し書く。メインの言語はJava。https://paranoiastudio-japan.jimdo.com/ に所属

【Java】【Hugo】ExcelTable_to_HugoTable

まあタイトル通りです。英語が正しいかは置いておいて。

HugoというMarkdownで記述できる静的サイトジェネレータを使用することがあるのですが、Markdownでテーブルを記述するのは非常に面倒です。

ExcelからMarkdownに変換するツールがWebアプリで転がっていたり、プラグインなんかで提供されていますが、Webアプリはセルの中で改行してたら上手くパースしてくれないし、プラグインをインストールするほどでもない(そもそもVS Codeは遊び以外では使わないし、IntelliJでは少し調べた感じ、ちょっと用途に合いそうなのが無かった)。

それに、HugoのMarkdownパーサーが若干他のと違います(違う気がするだけ?)。 HackMDというのを以前愛用していましたが、そっちでOKなものがHugoだとダメだったり。

ということで、自分の用途を考えるとそんなに難しいものでもないので作りました。 Javaではありますが、1ファイルに収まる && ライブラリなしなのですぐに動かせます。

満たしたかった要件

  • インストールとかそういったことはしたくない
  • 簡単に動かせること(本当はexeファイルにしたかった)
  • Excelのセル内改行に対応
  • Hugoで動くこと

ブログ用に多少は整えたけどクソコード感は否めない(´・ω・`)

import javafx.util.Pair; // 標準にあったから使っているだけで、Tuple等があるならそれでOK

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Main {
    /**
     * 引数に元ネタのテキストファイルを。同じディレクトリに__output.txtという名前で出力する
     */
    public static void main(String[] args) throws Exception {
        if (args == null || args[0] == null || args[0].isEmpty()) {
            System.out.println("Usage: java Main ${INPUT_FILE_ABSOLUTE_PATH}");
            return;
        }
        Path dir = Paths.get(args[0]);
        List<String> header;
        List<List<String>> dataList;
        {
            String pureTable = pureTable(dir, Charset.forName("Windows-31J")); // Windows環境なのでデフォだとこれになるので。。
            Pair<List<String>, List<List<String>>> pair = split(pureTable);
            header = pair.getKey();
            dataList = pair.getValue();
        }

        List<Integer> maxLengthPerColumn = maxLengthPerColumn(header, dataList);

        List<List<String>> table = toMarkdownTable(header, dataList, maxLengthPerColumn);

        String result = table.stream()
                .map(e -> String.join(" | ", e))
                .collect(Collectors.joining("\n"));

        Files.write(dir.getParent().resolve("__output.txt"), result.getBytes(StandardCharsets.UTF_8)); // outputはこっちで良いでしょう
    }

    /**
     * Excelの表組をテキストエディタ等に貼り付けた時にできる文字列(セル内に改行があると"で区切りられたりするアレ)を、改行を無視したプレーンなTSV形式にする
     */
    private static String pureTable(Path inputFile, Charset charset) throws Exception {
        String input = Files.lines(inputFile, charset)
                .filter(Objects::nonNull)
                .collect(Collectors.joining("\n"));


        StringBuilder builder = new StringBuilder();
        StringBuilder wkBuilder = new StringBuilder();
        boolean sameCell = false;
        boolean prevIsEscape = false;
        for (char c : input.toCharArray()) {
            if (c == '\\') {
                prevIsEscape = true;
                continue;
            }
            if (c == '"' && !prevIsEscape) {
                if (sameCell) {
                    builder.append(wkBuilder);
                } else {
                    wkBuilder = new StringBuilder();
                }
                sameCell = !sameCell;
            } else {
                if (sameCell) {
                    if (c != '\n') { // 改行は消してもええやろ
                        builder.append(c);
                    }
                } else {
                    builder.append(c);
                }
                prevIsEscape = false;
            }
        }
        return builder.toString();
    }

    /**
     * タイトル部とデータ部をそれぞれセル単位に分割する。
     */
    private static Pair<List<String>, List<List<String>>> split(String pureTable) {
        String[] wk = pureTable.split("\n", 2);
        String headerString = wk[0];
        String dataString = wk[1];

        List<String> header = Arrays.asList(headerString.split("\t", -1));
        List<List<String>> dataList = Arrays.stream(dataString.split("\n"))
                .map(e -> Arrays.asList(e.split("\t", -1)))
                .collect(Collectors.toList());

        return new Pair<>(header, dataList);
    }

    /**
     * それぞれの列で、タイトルとデータの最大長を返す
     */
    private static List<Integer> maxLengthPerColumn(List<String> header, List<List<String>> dataList) {
        List<Integer> list = new ArrayList<>(header.size());
        for (ListIterator<String> ite = header.listIterator(); ite.hasNext(); ) {
            int colIndex = ite.nextIndex();
            ite.next(); // 空読み
            list.add(maxLength(header, dataList, colIndex));
        }
        return list;
    }

    /**
     * 任意の文字を最大幅まで埋める(左詰めの右埋め)
     */
    private static String fill(String value, int max, char fix) {
        int valueLength = strLength(value);

        int diff = max - valueLength;
        if (diff <= 0) {
            return value;
        }
        StringBuilder sb = new StringBuilder(value);
        for (int i = 0; i < diff; i++) {
            sb.append(fix);
        }
        return sb.toString();
    }

    private static List<List<String>> toMarkdownTable(List<String> header, List<List<String>> dataList, List<Integer> maxLengthPerColumn) {
        // List[row[col]]
        List<List<String>> table = IntStream.range(0, 2 + dataList.size())
                .boxed() // mapToObj(ignore -> new ArrayList<String>())ではだめらしい
                .map(ignore -> new ArrayList<String>())
                .collect(Collectors.toCollection(ArrayList::new));

        for (ListIterator<Integer> ite = maxLengthPerColumn.listIterator(); ite.hasNext(); ) {
            int colIndex = ite.nextIndex();
            int max = ite.next();
            String headerText = fill(get(header, colIndex), max, ' ');
            table.get(0).add(headerText);
            String separatorText = fill("", max, '-');
            table.get(1).add(separatorText);

            int index = 2;
            for (List<String> data : dataList) {
                String dataText = fill(get(data, colIndex), max, ' ');
                table.get(index).add(dataText);
                index++;
            }
        }
        return table;
    }


    private static String get(List<String> list, int index) {
        int size = list.size();
        if (0 <= index && index < size) {
            return list.get(index);
        }
        return "";
    }

    private static int maxLength(List<String> header, List<List<String>> dataList, int colIndex) {
        int max = strLength(get(header, colIndex));
        for (List<String> data : dataList) {
            max = Math.max(max, strLength(get(data, colIndex)));
        }
        return max;
    }

    private static int strLength(String str) {
        return str.getBytes().length; // 色々考慮するならCodepointとか…?
    }
}