Rubyで列挙型（enum）っぽく連番を定義できるgemを作ってみた

## はじめに：列挙型について

C言語には列挙型（enum）があります。列挙型を使うと0から始まる連番を定数として管理できます。

```c
#include <stdio.h>

enum
{
    E_COLOR_RED,    // 0
    E_COLOR_YELLOW, // 1
    E_COLOR_GREEN,  // 2
};

int main()
{
    printf("E_COLOR_RED = %d\n", E_COLOR_RED);
    printf("E_COLOR_YELLOW = %d\n", E_COLOR_YELLOW);
    printf("E_COLOR_GREEN = %d\n", E_COLOR_GREEN);

    return 0;
}
```

実行結果

```
E_COLOR_RED = 0
E_COLOR_YELLOW = 1
E_COLOR_GREEN = 2
```

REDとYELLOWの間にBLUEを追加した場合、自動的に採番し直されます。

```c
#include <stdio.h>

enum
{
    E_COLOR_RED,
    E_COLOR_BLUE, // Add
    E_COLOR_YELLOW,
    E_COLOR_GREEN,
};

int main()
{
    printf("E_COLOR_RED = %d\n", E_COLOR_RED);
    printf("E_COLOR_BLUE = %d\n", E_COLOR_BLUE);
    printf("E_COLOR_YELLOW = %d\n", E_COLOR_YELLOW);
    printf("E_COLOR_GREEN = %d\n", E_COLOR_GREEN);

    return 0;
}
```

実行結果（1、2だったYELLOWとGREENはそれぞれ2、3に変わる）

```
E_COLOR_RED = 0
E_COLOR_BLUE = 1
E_COLOR_YELLOW = 2
E_COLOR_GREEN = 3
```

### Javaにも列挙型はある

列挙型はC言語のみならず、いろいろな言語で実装されています。たとえば以下はJavaで列挙型を使うコード例です。

```java
public class Sample {
    enum Color {
        RED,    // 0
        YELLOW, // 1
        GREEN   // 2
    }

    public static void main(String[] args) {
        System.out.println("Color.RED = " + Color.RED.ordinal());
        System.out.println("Color.YELLOW = " + Color.YELLOW.ordinal());
        System.out.println("Color.GREEN = " + Color.GREEN.ordinal());
    }
}
```

実行結果

```
Color.RED = 0
Color.YELLOW = 1
Color.GREEN = 2
```

### C# にもある

C# でもほぼ同じコードが書けます。

```csharp 
using System;

public class Sample
{
    enum Color
    {
        RED,    // 0
        YELLOW, // 1
        GREEN   // 2
    }
    
    public static void Main(string[] args)
    {
        Console.WriteLine("Color.RED = {0}", (int)Color.RED);
        Console.WriteLine("Color.YELLOW = {0}", (int)Color.YELLOW);
        Console.WriteLine("Color.GREEN = {0}", (int)Color.GREEN);
    }
}
```

実行結果

```
Color.RED = 0
Color.YELLOW = 1
Color.GREEN = 2
```

## でもRubyには列挙型がない！

しかし、Rubyには列挙型がありません。もしやるとするなら、愚直に定数を定義することになります。

```ruby
RED = 0
YELLOW = 1
GREEN = 2

puts "RED = #{RED}"
puts "YELLOW = #{YELLOW}"
puts "GREEN = #{GREEN}"
```

しかし、これだとBLUEを追加したときにYELLOWとGREENの修正も必要になります。

```diff_ruby
 RED = 0
+BLUE = 1
-YELLOW = 1
-GREEN = 2
+YELLOW = 2
+GREEN = 3
```

次のように工夫すれば修正量を少し減らすことができますが、あまりスマートではありません。

```ruby
RED = 0
BLUE = RED + 1
YELLOW = BLUE + 1
GREEN = YELLOW + 1
```

## そこで、enumっぽく連番を定義できるgemを作ってみた

そこで、Rubyでもenumっぽく連番を定義できるgemを作ってみました。
seq_as_enum というgemです。

https://github.com/JunichiIto/seq_as_enum/

インストール方法はふつうのgemと同じです。

```
gem install seq_as_enum
```

このgemはこんなふうに使います。

```ruby
require 'seq_as_enum'

class Sample
  extend SeqAsEnum

  # seq_as_enumメソッドで連番を定義
  seq_as_enum :RED, :YELLOW, :GREEN
  # すると、以下のような定数が定義される
  # RED = 0
  # YELLOW = 1
  # GREEN = 2

  # 上で定義した連番を使用する
  def self.main
    puts "RED = #{RED}"
    puts "YELLOW = #{YELLOW}"
    puts "GREEN = #{GREEN}"
  end
end

Sample.main
```

実行結果

```
RED = 0
YELLOW = 1
GREEN = 2
```

このgemを使えば、REDとYELLOWの間にBLUEを追加するときも必要最小限の修正で済みます。

```ruby
require 'seq_as_enum'

class Sample
  extend SeqAsEnum

  # REDとYELLOWの間にBLUEを追加する
  seq_as_enum :RED, :BLUE, :YELLOW, :GREEN

  def self.main
    puts "RED = #{RED}"
    puts "BLUE = #{BLUE}"
    puts "YELLOW = #{YELLOW}"
    puts "GREEN = #{GREEN}"
  end
end

Sample.main
```

実行結果（1、2だったYELLOWとGREENはそれぞれ2、3に変わる）

```
RED = 0
BLUE = 1
YELLOW = 2
GREEN = 3
```

## 応用的な使い方

このgemにはいくつか応用的な使い方があります。

### 初期値を変える

0以外の値からスタートしたい場合は`init`オプションを使います。

```ruby
seq_as_enum :RED, :YELLOW, :GREEN, init: 10
# 以下の定数を定義したのと同じ
# RED = 10
# YELLOW = 11
# GREEN = 12
```

### 文字列で連番を作る

A, B, Cのような連番を作ることもできます。

```ruby
seq_as_enum :RED, :YELLOW, :GREEN, init: 'A'
# 以下の定数を定義したのと同じ
# RED = 'A'
# YELLOW = 'B'
# GREEN = 'C'
```

### prefixを付ける

`prefix`オプションでprefixを付けることもできます。

```ruby
seq_as_enum :RED, :YELLOW, :GREEN, prefix: :COLOR
# 以下の定数を定義したのと同じ
# COLOR_RED = 0
# COLOR_YELLOW = 1
# COLOR_GREEN = 2
```

### Javaのenumのように型.定数の形式にする

`data_as`オプションを付けると、Javaのenumのように型.定数の形式で連番が定義できます。

```ruby
seq_as_enum :RED, :YELLOW, :GREEN, data_as: :Color
# 型.定数の形式で連番が定義できる
Color.RED    #=> 0
Color.YELLOW #=> 1
Color.GREEN  #=> 2
```

## 利用例

以下のコードは、HTMLをパースしてテーブル内の各列の内容を画面に出力するコード例です。
各列には以下のようなデータが入力されているものとします。

- 0番目 = 名前
- 1番目 = 住所
- 2番目 = 電話番号

ここでは `td[0]` `td[1]` `td[2]` のように列番号をハードコードする代わりに、seq_as_gem で定義した定数を使って `td[NAME]` `td[ADDRESS]` `td[PHONE]` のようにして各列の値を読み取っています。

```ruby
require 'seq_as_enum'
require 'nokogiri'

# パース対象のHTML
html = <<HTML
<table>
  <thead>
    <tr>
      <th>名前</th><th>住所</th><th>電話</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Alice</td><td>東京</td><td>090-1234-5678</td>
    </tr>
  </tbody>
</table>
HTML

class TableParser
  extend SeqAsEnum

  # 連番を定数として定義
  seq_as_enum :NAME, :ADDRESS, :PHONE

  def self.parse(html)
    doc = Nokogiri::HTML(html)
    doc.css('tbody tr').each do |tr|
      td = tr.css('td').map(&:text)
      # td[0]、td[1]、td[2]のように列番号をハードコードする代わりに定数を使う
      puts "name: #{td[NAME]}, address: #{td[ADDRESS]}, phone: #{td[PHONE]}"
    end
  end
end

TableParser.parse(html)
#=> name: Alice, address: 東京, phone: 090-1234-5678
```

## まとめ

というわけで、この記事ではRubyで列挙型（enum）っぽく連番を定義できる、seq_as_enum というgemを紹介してみました。

「なんでRubyには列挙型がないんだー！」と困っていた方はこちらのgemを試してみてください。

https://github.com/JunichiIto/seq_as_enum/