もっとお手軽な機械可読テキストテーブルフォーマット

前回の Mrtable の紹介エントリ を書いてたときにもっとお手軽なものを思いつき、「あれっ、桁を揃えるだけならひょっとしてこんなので良かった?」と思ってしまったのでメモ。

こんなの:

[ "c1" , "c2"           , "c3"  , "c4"                   ]
[    1 , "a"            , ""    , null                   ]
[ " "  , "null"         , 12.34 , "\\\t\r\n\"\\\t\r\n\"" ]
[ 1234 , "全角テキスト" , ""    , ""                     ]

要するに1行が1個の配列で、それを JSON にして、桁を揃えるだけ。頻繁に手書きするのでなければこれで十分では。

パースは行ごとに JSON.parse() すればいい(実際これだけなのでライブラリ化する必要もないくらい)として、桁揃えも Mrtable のを流用すればすぐできそう、ということでちゃちゃっと書いてみました。

require 'json'

module JsonArrayTable

  def self.parse(text)
    text.lines.map{ |line| JSON.parse(line) }
  end

  def self.generate(rows)
    num_cols = rows[0].size

    serialized =
      map_col_with_ci(rows) do |col, _|
        col.to_json
      end

    max_lens =
      (0...num_cols).map do |ci|
        col_len_max(serialized, ci)
      end

    padded =
      map_col_with_ci(serialized) do |col, ci|
        pad_col(col, max_lens[ci])
      end

    lines =
      padded.map do |cols|
        "[ " + cols.join(" , ") + " ]\n"
      end

    lines.join("")
  end

  private

  def self.map_col_with_ci(rows)
    rows.map do |cols|
      indexes = (0...cols.size).to_a
      cols.zip(indexes).map do |col_ci|
        yield *col_ci
      end
    end
  end

  def self.col_len_max(rows, ci)
    rows
      .map{ |cols| col_len(cols[ci]) }
      .max
  end

  # 32-126(0x20-0x7E), 65377-65439(0xFF61-0xFF9F)
  def self.hankaku?(c)
    /^[ -~。-゚]$/.match?(c)
  end

  def self.col_len(col)
    col.chars
      .map{ |ci| hankaku?(col[ci]) ? 1 : 2 }
      .sum
  end

  def self.pad_right(s, n)
    rest = n - col_len(s)
    return s if rest == 0
    s + (" " * rest)
  end

  def self.pad_left(s, n)
    rest = n - col_len(s)
    return s if rest == 0
    (" " * rest) + s
  end

  def self.int?(s)
    /^\-?[\d]+$/.match?(s)
  end

  def self.pad_col(col, maxlen)
    if int?(col)
      pad_left(col, maxlen)
    else
      pad_right(col, maxlen)
    end
  end

end

例:

require 'pp'

text = <<-'EOB'
["c1", "c2", "c3", "c4"]
[1, "a", "", null]
[" ", "null", 12.34, "\\\t\r\n\"\\\t\r\n\""]
[1234, "全角テキスト", "", ""]
EOB

rows = JsonArrayTable.parse(text)
pp rows

=begin

[["c1", "c2", "c3", "c4"],
 [1, "a", "", nil],
 [" ", "null", 12.34, "\\\t\r\n" + "\"\\\t\r\n" + "\""],
 [1234, "全角テキスト", "", ""]]

=end

puts JsonArrayTable.generate(rows)

=begin

[ "c1" , "c2"           , "c3"  , "c4"                   ]
[    1 , "a"            , ""    , null                   ]
[ " "  , "null"         , 12.34 , "\\\t\r\n\"\\\t\r\n\"" ]
[ 1234 , "全角テキスト" , ""    , ""                     ]

=end

適当に JsonArrayTable という名前にしましたが、もうこれでいいかな……命名のセンスなくて悲しみ。 JSON を抜かして array table とか…… 誰かかっこいい名前を考えてください……。

(2021-04-18 追記) array table でいいかな……という気分になってきています。