読者です 読者をやめる 読者になる 読者になる

波打際のブログさん

主に、プログラミング備忘録など。

RubyでInteger値を持つEnum、Inumを作ってみた。

この記事はバージョン2.x系の記事で、現行バージョンの3.xとは互換性がありません。

RailsのEnumと相性が悪い人向けのInumをバージョンアップさせた話。 - 波打際のブログさん
こちらの記事がバージョン3.x系の記事となります。

はじめに

 最近Rubyを使用する機会が増えてきたのですが、Rubyに列挙型の実装がありませんでした。そこで、Gemを探してみたものの、C言語ライクなInteger値を持つEnumで、思ったとおりの事が可能なGemが存在しなかったので、自前で実装する機会がありました。その実装の仕組みを参考に汎用的に使用可能なEnum(Inum)を作成してGemとして公開しました。


  • 2013年11月28日、機能追加に伴い追記しました。
  • 2013年12月03日、バージョン2.0.0リリースに伴い記事を修正しました。
  • 2013年12月10日、バージョン2.0.5リリースに伴い記事を修正しました。

Inumでやりたいこと(実装したこと)

  • CやC#ライクなInteger値を保持しているEnumで有ること。
  • 列挙型のメンバーを表すインスタンスは、一つしか存在しないこと。(メンバーがシングルトン)
  • 値とメンバー名の相互変換ができること。
  • i18nを使用して列挙型からローカライズした文字列を取得できること。

使い方

Gemのインストール

 いつもどおりです、RailsならGemfileなどにinumの依存関係を追加します。

gem 'inum'

列挙型の定義

 Inum::Baseを継承したクラス内で、define_enumメソッドを呼び出して列挙型を定義します。define_enumの第一引数には列挙型のラベル(名前)をSymbolで、第二引数には列挙型の値をIntegerでそれぞれ渡します。第二引数の列挙型の値は省略可能で、省略された場合0から自動的にインクリメントされます。

class AnimeType < Inum::Base
  define_enum :NyarukoSan,   0
  define_enum :KiniroMosaic, 1
  define_enum :KillMeBaby,   2
end

列挙型の使用方法

Inumを継承した列挙型は次のように使用出来ます。

p AnimeType::NyarukoSan       # => NyarukoSan 
p AnimeType::NyarukoSan.to_i  # => 0

# 数値や、シンボルから列挙型へのパーズが可能です。
type = AnimeType::parse(1)
p type.equal?(AnimeType::KiniroMosaic)  # => true

# eql?メソッドを使用すると、比較対象のオブジェクトをパーズ後比較します。
p AnimeType::KillMeBaby.eql?('KillMeBaby')  # => true

# underscoreメソッドを使用すると、Enumのラベルをアンダースコアつなぎの文字列にします。
p AnimeType::NyarukoSan.underscore  # => nyaruko_san

# +と-の単項演算子も定義されています。
p AnimeType::NyarukoSan + 1  # => KiniroMosaic
p AnimeType::KillMeBaby - 1  # => KiniroMosaic

# eachによる繰り返しもできます。
AnimeType::each do |enum|
  p enum
end

Enumerableモジュールをextendし、Comparableモジュールをincludeしていますので、mapなどのメソッドが利用可能です。

その他のメソッドについてはドキュメント(Class: Inum::Base — Documentation for alfa-jpn/inum (master))を参照してください。

その他の使い方

i18nを利用する

 Enum(Inum)で to_t メソッドを利用することで、i18nローカライズされた文字列を取得することが可能です。

次のような多言語化ファイルが存在した場合

ja:
  inum:
    anime_type:
      nyaruko_san:   "這いよれ!ニャル子さん"
      kiniro_mosaic: "きんいろモザイク"
      kill_me_baby:  "キルミーベイベー"

translateメソッドを用いると次の結果になります。

p AnimeType::NyarukoSan.translate    # => 這いよれ!ニャル子さん
p AnimeType::KiniroMosaic.translate  # => きんいろモザイク
p AnimeType::KillMeBaby.translate    # => キルミーベイベー

i18nの辞書定義はデフォルトでは inum.{名前空間(モジュール名)}.{クラス名}.{メンバー名} を参照しますが、i18n_keyクラスメソッドをオーバーライドすることで参照先を書き換えることも可能です。

class AnimeType < Inum::Base
  define_enum :NyarukoSan,   0
  define_enum :KiniroMosaic, 1
  define_enum :KillMeBaby,   2

  # i18n_keyをオーバーライド(内容はデフォルトの定義です。)
  def self.i18n_key(label)
    # underscoreメソッドでキャメルをアンダースコアつなぎにした上で、::を.に置き換えています。
    Inum::Utils::underscore("inum::#{self.name}::#{label}")
  end
end
Enumをクラスの変数に結びつける

 DefineCheckMethodモジュールを利用すると、クラスの変数とEnumを関連付けし、判別用メソッドを自動的に定義することが可能です。

class Anime
  extend Inum::DefineCheckMethod

  attr_accessor :type

  define_check_method :type, AnimeType
end

define_check_methodにカラム名と関連付けさせるInumを継承したEnumクラスを渡して判別用メソッドを自動的に定義しています。

anime = Anime.new
anime.type = AnimeType::NyarukoSan

p anime.nyaruko_san?    # => true
p anime.kiniro_mosaic?  # => false


# typeにはパーズ可能なメソッドなら何でも入ります。
anime.type = 1
p anime.kiniro_mosaic? # => true

また、define_check_methodの第三引数は省略可能オプションになっていて、prefixの設定が可能です。prefixを指定すると定義されるメソッドの先頭にprefixが付与されます。

class Anime
  extend Inum::DefineCheckMethod

  attr_accessor :type

  define_check_method :type, AnimeType, 'anime'
end

prefixにanimeを指定すると、定義されるメソッドは次のようになります。

anime = Anime.new
anime.type = AnimeType::NyarukoSan

p anime.anime_nyaruko_san?    # => true

おわりに

 何か改善案あればpullrequest、issue気軽に投げてください、githubアカウントお持ちでない方はこのブログのコメント欄にもお気軽にどうぞ。

関連リンク