CSVライブラリからTSVライブラリを作る　～col_sepなんて指定したくない～

# 動機
スクレイピング結果をTSVで保存していくが、5回目ぐらいから`col_sep: "\t"`を書くのが苦痛になってきた。
`require "tsv"; tsv = TSV.open("path/to/file.tsv", "w");`みたいに書きたい！__書いてやる！！！__

# 解答
ご自身の`site_ruby`フォルダに下記のファイルを配置するだけ。

```tsv.rb
# frozen_string_literal: true

require "csv"

class TSV < CSV
  DEFAULT_OPTIONS = {
    col_sep:            "\t",
    row_sep:            :auto,
    quote_char:         '"',
    field_size_limit:   nil,
    converters:         nil,
    unconverted_fields: nil,
    headers:            false,
    return_headers:     false,
    header_converters:  nil,
    skip_blanks:        false,
    force_quotes:       false,
    skip_lines:         nil,
    liberal_parsing:    false,
    quote_empty:        true,
  }.freeze

  def initialize(data,
                 col_sep: "\t",
                 row_sep: :auto,
                 quote_char: '"',
                 field_size_limit: nil,
                 converters: nil,
                 unconverted_fields: nil,
                 headers: false,
                 return_headers: false,
                 write_headers: nil,
                 header_converters: nil,
                 skip_blanks: false,
                 force_quotes: false,
                 skip_lines: nil,
                 liberal_parsing: false,
                 internal_encoding: nil,
                 external_encoding: nil,
                 encoding: nil,
                 nil_value: nil,
                 empty_value: "",
                 quote_empty: true)
    super
  end
end
```

# 解答に至るまで
CSVライブラリで`col_sep: "\t"`と指定されていれば、望むTSVライブラリそのものです。
CSVライブラリで用いられる__キーワード引数__のデフォルト値だけ変更したTSVクラスを定義し、そのファイルを`$:`(= `$LOAD_PATH`= `$-I`)のどこかに配置すれば良いでしょう。


## CSVライブラリの観察
まずCSVライブラリがどこにあるのかを探します。
ライブラリがどこにあるのか探すコマンド `gem which` を用いましょう。

```
>gem which csv
C:/Ruby261-x64/lib/ruby/2.6.0/csv.rb

# こんな感じに配置されている
C:/Ruby261-x64/lib/ruby/2.6.0
├── 他のファイル・ディレクトリ郡
├── csv.rb
└── csv
    ├── core_ext
    │   ├── array.rb
    │   └── string.rb
    ├── fields_converter.rb
    ├── match_p.rb
    ├── parser.rb
    ├── row.rb
    ├── table.rb
    ├── version.rb
    └── writer.rb
```

これらのファイルからキーワード引数`col_sep`と重要そうな部分だけ見ていきます。

```csv.rb

class CSV

  # 略

  DEFAULT_OPTIONS = {
    col_sep:            ",", # ←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←
    row_sep:            :auto,
    quote_char:         '"',
    field_size_limit:   nil,
    converters:         nil,
    unconverted_fields: nil,
    headers:            false,
    return_headers:     false,
    header_converters:  nil,
    skip_blanks:        false,
    force_quotes:       false,
    skip_lines:         nil,
    liberal_parsing:    false,
    quote_empty:        true,
  }.freeze

  # 略

  def initialize(data,
                 col_sep: ",", # ←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←
                 row_sep: :auto,
                 quote_char: '"',
                 field_size_limit: nil,
                 converters: nil,
                 unconverted_fields: nil,
                 headers: false,
                 return_headers: false,
                 write_headers: nil,
                 header_converters: nil,
                 skip_blanks: false,
                 force_quotes: false,
                 skip_lines: nil,
                 liberal_parsing: false,
                 internal_encoding: nil,
                 external_encoding: nil,
                 encoding: nil,
                 nil_value: nil,
                 empty_value: "",
                 quote_empty: true)
    raise ArgumentError.new("Cannot parse nil as CSV") if data.nil?

    # create the IO object we will read from
    @io = data.is_a?(String) ? StringIO.new(data) : data
    @encoding = determine_encoding(encoding, internal_encoding)

    @base_fields_converter_options = {
      nil_value: nil_value,
      empty_value: empty_value,
    }
    @initial_converters = converters
    @initial_header_converters = header_converters

    @parser_options = {
      column_separator: col_sep, # ←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←
      row_separator: row_sep,
      quote_character: quote_char,
      field_size_limit: field_size_limit,
      unconverted_fields: unconverted_fields,
      headers: headers,
      return_headers: return_headers,
      skip_blanks: skip_blanks,
      skip_lines: skip_lines,
      liberal_parsing: liberal_parsing,
      encoding: @encoding,
      nil_value: nil_value,
      empty_value: empty_value,
    }
    @parser = nil

    @writer_options = {
      encoding: @encoding,
      force_encoding: (not encoding.nil?),
      force_quotes: force_quotes,
      headers: headers,
      write_headers: write_headers,
      column_separator: col_sep, # ←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←
      row_separator: row_sep,
      quote_character: quote_char,
      quote_empty: quote_empty,
    }

    @writer = nil
    writer if @writer_options[:write_headers]
  end

 # 略

end
```

他に重要そうなファイルはありませんでした。
`csv.rb`の`DEFAULT_OPTIONS`と`initialize`の`col_sep`の値を変更すれば良さそうです。

## TSVライブラリを作る

わざわざCSVライブラリを構成する全ファイルをどこかにコピーして編集して…なんて面倒です。
CSVクラスを継承したTSVクラスを定義し、`col_sep`の部分だけ変えます。
コードは解答を参照。

引数のデフォルト値を変えただけなので、`initialize`の中身はただの`super`でOKです。
`super`は、[メソッド呼び出し(super・ブロック付き・yield) (Ruby 2.6.0)](https://docs.ruby-lang.org/ja/latest/doc/spec=2fcall.html#super)で以下のように書かれています。

>super は現在のメソッドがオーバーライドしているメソッドを呼び出します。括弧と引数が省略された場合には現在のメソッドの引数がそのまま引き 渡されます。


## TSVライブラリを`require`できる場所に配置する

自分の環境で`require`できる場所がどこか確認してみます。

```
>ruby -e 'puts $:'
C:/Ruby261-x64/lib/ruby/gems/2.6.0/gems/did_you_mean-1.3.0/lib
C:/Ruby261-x64/lib/ruby/site_ruby/2.6.0
C:/Ruby261-x64/lib/ruby/site_ruby/2.6.0/x64-msvcrt
C:/Ruby261-x64/lib/ruby/site_ruby
C:/Ruby261-x64/lib/ruby/vendor_ruby/2.6.0
C:/Ruby261-x64/lib/ruby/vendor_ruby/2.6.0/x64-msvcrt
C:/Ruby261-x64/lib/ruby/vendor_ruby
C:/Ruby261-x64/lib/ruby/2.6.0
C:/Ruby261-x64/lib/ruby/2.6.0/x64-mingw32
```

置き場所が沢山あって困りますね。[variable $-I (Ruby 2.6.0)](https://docs.ruby-lang.org/ja/latest/method/Kernel/v/=2dI.html)を参考にしましょう。
私はベンダーでもないですし、TSVライブラリはCSVライブラリに依存しているのでバージョン依存でもないでしょう。
`site_ruby`(`C:/Ruby261-x64/lib/ruby/site_ruby`)に配置することにしました。

`anyenv`や`rbenv`を使用している場合は、環境変数`RUBYLIB`を設定し、そこに配置すればOKです。
参考：[環境変数 (Ruby 2.6.0)](https://docs.ruby-lang.org/ja/latest/doc/spec=2fenvvars.html)

# 感想

EASYで助かった。