タブ区切りのテキストファイルをスペースでイイカンジに整形するアプリ作った

タブ区切りのテキストファイル、いわゆる TSV ファイルがあるのだが、タブ区切りそのままでは見づらい。

その TSV を読み込むシステムは、タブ文字でも連続したスペース文字でも、どちらでもデータを区切れていれば良いので、スペースを使ってアラインメントを揃えてデータを区切ることにした。

例でいうと、こんな感じの TSV データがある。 ↓

# 元となる TSV のサンプル。タブ1文字をスペース4文字で再現している
HOGE    90    normal    200
FOO    -5.1    sp    1800
BAR    1.80    normal    900
ぴよぴよ    99.1    sp    1500

データの内容によって左揃え or 右揃えにしたい、とか、小数を含む数値の列は、整数部分は右揃え・ピリオドを挟んで小数部分は左揃えになると、桁が揃って読みやすいなと思った。

例でいうと、こんな風にスペースを使って、アラインメントを揃えて整形したい。↓

# 左揃え    数値     右揃え    数値
HOGE        90       normal     200
FOO         -5.1         sp    1800
BAR          1.80    normal     900
ぴよぴよ    99.1         sp    1500

「ぴよぴよ」が含まれる列のように、全角文字が入ってもイイカンジにスペースの数を調整したい。

ウェブアプリ作りました

そんなウェブアプリを Vanilla JS で実装してみた。以下で試していただける。

マジで愚直に文字数を数えてスペース文字を付与している。以下、実装詳細のメモ。

目次

半角・全角文字を区別する正規表現

スペースを付与する関数は padStart()padEnd() があるので、あとはいくつスペースを付与してやれば良いかを調整した。

全角文字を「半角文字2つ分の幅」と雑に捉えてスペースの数を算出することにしたので、半角文字のみを抽出する正規表現を探した。以下で取得できた。

// 元となる文字列
const originalText = '0aA!あア亜';

// 以下で半角文字を全て除去できる
const fullWidthCharactersOnly = originalText.replace(/[ -~]/gu, '');
// → 'あア亜' となる

[ -~] で半角英数字や記号など、ASCII 文字はひととおり拾えるみたい。

あとは length でテキトーに文字数をカウントし、それぞれを加減算する。

整数部を右揃え・小数部を左揃えにする

数値列の整数部と小数部の境界をもって数値の桁位置を揃えるために、次のような正規表現でパーツを別けた。

const matches = value.match((/^([-0-9]*)+(\.?)+([0-9]*)$/));

if(matches != null) return console.log('変数 value は数値ではない様子');

const integer = matches[1];  // 整数部分の文字列
const period  = matches[2];  // 小数点・ピリオド
const decimal = matches[3];  // 小数部分の文字列

あとは整数部分・小数部分それぞれで「その列内で最長の文字数」をカウントして、それに合わせて padStart()padEnd() でスペースを付与して結合している。「整数部分は4行目の 1000 が4桁で最長」「小数部分は2行目にある -1.08922 の5桁が最長」という感じでカウントしたら、「整数部は4桁で右揃え」「小数点ないしはスペース」「小数部は5桁で左揃え」という感じで整形しているワケである。

「行→列」の二次元配列を「列→行」の二次元配列に変換する

TSV ファイルのデータを受け取ると、改行コードで区切って行ごとの配列を作ったら、1行ごとにタブ文字で区切って列を分割した、二次元配列を組み立てるのが大筋だろう。

しかし今回は、1列ごとに文字数をカウントしてスペースを付与する必要があったので、「1列のデータ」を配列で取得したかった。

そこで調べていくと、Python の numpy には transpose という関数があり、二次元配列の親子を入れ替えられるそうだ。それ相当の処理を JavaScript で行う実装があったので、それを参考にした。

シンプルなワンライナーで行列を入れ替えられるのだが、このワンライナーには問題があって、「1行目の列数をベースにして行列を入れ替える」ため、例えば1行目が2列、2行目以降が3列、といったデータを投げ込むと、3列目のデータがごっそり欠落してしまうのである。綺麗でない TSV だと、行ごとに列数が異なる場合もあるかと思い、このワンライナーをベースにして以下のように実装してみた。

/** 二次元配列の行列を入れ替える・列数が不揃いでも対応できるようにする */
const transpose = rows => {
  // 行列の二次元配列から、行数と、列の最大数を取得する
  const rowNumber    = rows.length;
  const columnNumber = Math.max(...rows.map(columns => columns.length));
  
  const newColumns = [];
  for(let columnIndex = 0; columnIndex < columnNumber; columnIndex++) {
    const newRows = [];
    for(let rowIndex = 0; rowIndex < rowNumber; rowIndex++) {
      newRows[rowIndex] = rows[rowIndex][columnIndex];  // 指定の行列に値がなければ undefined が埋められることになる
    }
    newColumns[columnIndex] = newRows;
  }
  return newColumns;
};

定数 columnNumber がミソ。1行ごとの列数を集計し、Math.max() で最大値を取得している。コレにより、一番多い列の数を基準にして二次元配列を組み立てている。列数が不足しているところは、変換後に undefined が埋まるようになり、データの欠落は発生しなくなる。

イベント操作と DOM 操作はやはりキツいが…

自分は TypeScript じゃなくても別に書けちゃうので、Polyfill なしに async や Optional Chaining などが使えるようになってきた最近は、Vanilla JS でとても簡単に色んなことができて重宝している。

しかし、DOM 操作やイベント制御に関しては、未だ Vanilla でやると煩雑なところが多く、何らかの SPA フレームワークを使いたくなる。

今回は Vanilla JS に Vanilla CSS で、外部ライブラリの利用なしに全処理・デザインの実装をしてしまったが、こういう能力はそろそろ頑張らなくても良い気がしてきたな。こういうことばっかりやってるとショートハンドとして jQuery が欲しくなる気持ちも何となく分かる。ただ、外部ライブラリを読み込むのは依存している感じが気持ち悪くて嫌なんだよなぁ。特に CodePen ぐらいでチャチャッと作る時に、自分が書いたコードだけで全てが動かないというのが気持ち悪い。どうしたものか。w

あと、今回はシングルページでアプリを作ってるので、グローバルな関数をボンボン作っちゃったのだが、関数式で書くのと関数宣言で書くのとで、「巻き上げ」以外に機能差があるのかよく知らない。巻き上げの副作用も理解していて、ギョーミーな実装で多用するつもりはないが、今回のような小さな実装では巻き上げを使えると楽な場面も多い。ただ関数宣言はアロー関数の形では書けないので、ワンライナーで良いような関数は関数式として書きたいところもある。この辺雰囲気で書いている。w

とりあえずコレで当初やりたかったことは出来たので以上。