Newer
Older
csv2table / csv2table.rb
#!/usr/bin/env ruby22
# coding: euc-jp
# CSV to table(HTML) converter - csv2table.rb
# (c)2000, 2007, 2017 by HIROSE, Yuuji <yuuji@gentei.org>
# $Id$
# Last Modified Mon Dec 11 11:15:46 2017 on firestorm

=begin

CSVもしくはそれに準ずる(TAB区切りとの併用など)形式で書いた
データファイルを HTMLの <table> に変換する。HTMLテキストからは
SSIによって csv2table.rb を利用することを想定している。

変換元はCSVだけでなく、Rubyライブラリ Rooがある場合はODS(Calc表計算ファ
イル)でもよい。データファイルの先頭行のCSV列がtableの見出し(th)として扱
われ、見出し項目の後ろに :l :c :r をつけると対応する(二行目以降の)カラム
がそれぞれ 左寄せ、中央寄せ、右寄せ される。

例:
*元ファイルが純正CSV(sample.csv)
	氏名,住所,電話番号:r
	慶應太郎,東京都港区,01-2345-6789
	山口燃え,山口県防府市,09-9999-8765
		↓
	./csv2table.rb sample.csv
		↓
	<table border="0">
	 <tr><th>氏名</th><th>住所</th><th align="right">電話番号</th></tr>
	 <tr><td>慶應太郎</td><td>東京都港区</td>
		<td align="right">01-2345-6789</td></tr>
	 <tr><td>山口燃え</td><td>山口県防府市</td>
		<td align="right">09-9999-8765</td></tr>
	</table>

データ区切りのカンマの前後に空白を許したいときは -s オプション
カンマではなくTAB区切りにしたいときは -t オプションを利用する。

また、-e オプションにより特定のカラムの内容を、別カラムから生成すること
ができる。利用できるマクロは以下の通り。
	$n		nカラム目の値に置換される
	$prev[n]	nカラム目の直前の行の値に置換される
	$sum[n]		nカラム目の直前の行までの総和
	$diff[n]	nカラム目の現在行と直前の行の差
	(それぞれ [n] を省略するとカレントカラム指定になる)

また、$ を利用したカラムがあると、そのカラムは上記置換をした上で数式
評価が行われる。しがたって、

	単価:r,個数:r,金額:r
	50,3,$0*$1
	20,2,$0*$1

	は、
	+----+----+----+
	|単価|個数|金額|
	+----+----+----+
	|  50|   3| 150|
	+----+----+----+
	|  20|   2|  40|
	+----+----+----+
という表に変換される。$n の置換は、数式置換が行われるよりも前に行われる
ので数式の結果を $n で取り込むことはできないことに注意する。

大きな表で、スクロールさせると項目名が見えなくなる場合は行ガイド、列ガイド
を出せる。
	-row 20
とすると、20行毎に1行目と同じ行が出る。
	-col 30
とすると、30カラムごとに1カラム目と同じ項目が出る(カラムガイド)。なお、
カラムガイドのtd要素には自動的に class="colguide" が付加されるので、HTML
head要素内で

<style type="text/css">
<!--
th {background: #ff9;}
td.colguide {background: #fdf;}
-->
</style>

などと記述しておけば、カラムガイド列が別の色になって判別しやすくなる。

さらに、デフォルトで奇数行のtr要素に class="odd"、偶数行のtr要素に
class="even" が付加される。CSSで
  tr.even {background: #eff;}
  tr.odd  {background: #dff;}
などとしておけば1行おきに背景色を変えて見分けやすくすることができる。

その他のオプションは ./csv2table.rb -h で参照。
=end

# 本体ここから
require 'kconv'
require 'csv'

# デフォルト値
$border="0"
$numbering=false
$strip=false
$colalign=[]
$tabdelim=false
$expand=false
$rowguide=0
$colguide=0

def usage
  print <<_EOU_
#{$0} [options] CSV-files...
Options are...
  -border N	Set border width of the table to N
  -n		Enable row numbering
  -t		Accept TAB as a field delimiter
  -s		Strip entry-surrounding white spaces
  -e		Enable expression expansion
		($sum[n], $diff[n], $prev[n], $n, and math-exp.)
  -row N	Put heading line each N rows
  -col N	Put column guide each N columns
_EOU_
end

while /^-([a-z]+)$/no =~ ARGV[0]
  case $1
    when "border"
    ARGV.shift
    $border=ARGV[0]
    when "n"
    $numbering=true
    when "s"
    $strip=true
    when "t"
    $tabdelim=true
    when "e"
    $expand=true
    when "row"
    ARGV.shift
    $rowguide=ARGV[0].to_i
    when "col"
    ARGV.shift
    $colguide=ARGV[0].to_i
    when "h"
    usage; exit 0
  end
  ARGV.shift
end

def putrow(list, n, elm = "td")
  cgclass=" class=\"colguide\""
  rgclass=" class=\"rowguide\""
  rowclass = if n > 0
	       " class=\"" + (n%2 == 0 ? "even" : "odd") + "\""
	     else
	       ""
	     end
  print " <tr#{rowclass}>"
  if $numbering
    print "<#{elm} align=\"center\">"
    if n > 0 then print n else print "番号" end
    print "</#{elm}>"
  end
  i=0
  list.each{|l|
    if /\$\d/ =~ l
      l.gsub!(/\$(\d+)/){|n| list[$1.to_i]}
      l.strip!
      STDERR.print "---------------l=#{l}\n" if $DEBUG
      begin
	list[i] = l = (eval l).to_s
      rescue
      end
      STDERR.print "---------------l2=#{list[i]}\n" if $DEBUG
    end
    if /^[-0-9. ]+$/ =~ l
      $diff[i] = l.to_f - $prev[i].to_f if $prev[i]
      # $prev[i] = l.to_f
      $sum[i] += l.to_f
    end
    i+=1
  } if $expand
  i=0
  col0=''
  list.each{|l|
    l = l||""
    $strip and l.strip!
    if l <= "" then  l = "<br>" end
    l.sub!(/^[ \t]+$/, "<br>")
    out = l
    if $expand
      if /\$/ =~ l
	l.gsub!(/(sum|diff|prev)(?!\[)/){|n| n + "[#{i}]"}
	#if /(sum|diff)(?!\[)/ =~ l then l << "[i]" end
	print("--- l=[#{l}]\n") if $DEBUG
 	l.gsub!(/\$(sum|diff|prev)\[(\d+)\]/){|x|
 	  eval("\$#{$1}[#{$2}]")
 	}
 	print("--- l2=[#{l}]\n") if $DEBUG
        out = eval("#{l}") # "$sum[i].to_s"
      end
      $nprv[i] = out
      if out.is_a?(Numeric) \
	|| (out.is_a?(String) && /^[0-9]+\.[0-9]+$/ =~ out)
	out = (sprintf("%10.2f", out)).to_f
      end
    end
      #STDERR.print("out -> #{out}\n");
    col0=out if i==0
    tdc = (i==0 ? cgclass : '')
    if i > 1 && $colguide > 0 && i%$colguide == 0 then
      print "<#{elm}#{cgclass}#{$colalign[i]}>#{col0}</#{elm}>"
    end
    print "<#{elm}#{tdc}#{$colalign[i]}>#{out}</#{elm}>"
    i+=1
  }
  $prev[0..-1] = $nprv
  print "</tr>\n"
end

# 本体
count = 0
print "<table border=\"#{$border}\">\n"
# 一行目は見出し

case ARGV[0]
when /\.ods/i
  require 'roo'
  data = CSV.parse(Roo::OpenOffice.new(ARGV[0]).to_csv.toeuc)
else
  str = ARGF.read.toeuc
  str.gsub!(/\t*,/, ",") if $tabdelim
  data = CSV.parse(str)
end

th = nil
data.each do |l|
  next if /^$|^\#/ =~ l[0]			# Skip Comment Line
  if count == 0
    th = l
    $sum=Array.new(th.length, 0.0)
    $diff=Array.new(th.length, 0.0)
    $prev=Array.new(th.length, 0.0)
    $nprv=Array.new(th.length, 0.0)
    i=0
    th.each{|e|
      if /:([lcr])$/io =~ e
	$colalign[i] = sprintf(" align=\"%s\"",
			       {"l"=>"left", "c"=>"center", "r"=>"right"}[$1])
	e.sub!(/:[lcr]$/, "")
      end
      i+=1
    }
    putrow(th, 0, "th")
  else
    if $rowguide > 0 && count%$rowguide == 0
      putrow(th, 0, "th")
    end
    putrow(l, count) if l
  end
  count+=1
end
print "</table>\n"