タイトルモデルの作成

背景

chisato-moritaka-db の時には、日本語・英語の振り分けを Mix-in の形で登録していました。今回は、さまざまなタイトルを個々のテーブルに持つのではなく、Title モデルを導入することにしました。タイトル一覧のページを作ることで、同一タイトルのオブジェクトの作成を簡単にするだけでなく、索引としての役割をもたせるためです。

Title モデルは japanese, english, yomi, yomi_suuji の 4 つの文字列属性を持ちます。yomi_suuji は読みを数値化したもので、データベース上で辞書順ソートができるようにしたものです。姓名をそれぞれ小数点表示をし、間を「-」でつなぎます。それぞれの整数部は濁点なしの文字に変換後に対応するコードに置き換えたもので設定し、小数部は濁点、半濁点のフラグで設定します。例えば「こばやし」であれば、「14507021.0100」となり、「こはら」であれば「145080.000」となります。文字で比較すると、「こばやし」が後ろになってしまいますが、変換した数値では正しく前になることがわかります。姓名の間を数字よりもアスキーコードが若い「-」に設定しているので、「こば」は「こばやし」よりも前に並びます。なお、この yomi_suuji は yomi 属性から自動的に生成されるものとします。

データベーステーブルの設定

まず、migration ファイルを generator で作成します。

r g model title japanese:string english:string yomi:string yomi_suuji:string

以下のファイルが作成されます。factory_girl の spec も生成されています。

      invoke  active_record
      create    db/migrate/20140202232237_create_titles.rb
      create    app/models/title.rb
      invoke    rspec
      create      spec/models/title_spec.rb
      invoke      factory_girl
      create        spec/factories/titles.rb

上記の属性はすべて not null を期待しているので、migration に null:false を設定しておきます。

class CreateTitles < ActiveRecord::Migration
  def change
    create_table :titles do |t|
      t.string :japanese, null:false
      t.string :english, null:false
      t.string :yomi, null:false
      t.string :yomi_suuji, null:false

      t.timestamps
    end
  end
end

データベースを migration しておきます。テストデータも準備しておきます。

be rake db:migrate
be rake db:test:prepare

FactoryGirl の設定

Factory Girl を使う前に関連設定をしておきます。gaurd init しただけでは、FactoryGirl ファイルの watch 設定がされていないので、FactoryGirlのファイルが変更されたタイミングでテストを自動的に実行するを参考に Guard ファイルに以下の設定を行います。

  # FactoryGirl
  watch(%r{^spec/factories/(.+)\.rb$}) { |m| [ "spec/models/#{m[1].singularize}.rb", "spec/controllers/#{m[1]}_controller_spec.rb" ] }

ここで、単数形を求めるために singularize を使用しているため、Guardfile の先頭で inflector.rb を require しておきます。

require 'active_support/inflector'

また、FactoryGirl 更新時に変更が適用されるように、config/application.rb に FactoryGirl のリロードを追加しておきます。

  # FactoryGirl
  config.before(:all) do
    FactoryGirl.reload
  end

テスト及び実装のイテレーション

まず、spec/factories/titles.rb を修正します。

FactoryGirl.define do
  factory :title do
    japanese "日本語 英語"
    english "English Japanese"
    yomi "にほんご えいご"
    yomi_suuji "ダミーデータ"
  end
end

この設定の結果、FactoryGirl.build(:title) とすることで、必要な属性を持った Title オブジェクトが作成されます。これを使って spec を書いていきます。まず、FactoryGirl でオブジェクトが作成できることを確認します。

describe Title do
  let(:title) { FactoryGirl.build(:title) }

  it '妥当なオブジェクトが生成されること' do
    expect(title).to be_valid
  end
end

ここで let は title という特殊なメソッドを定義しているものです。初回はオブジェクトを作成しますが、二回目以降は最初に生成したオブジェクトを返します。以前は以下のようなメソッドを自分で作成していましたが、それが簡単にかけるようになった感じでしょうか。

  def title; @title ||= FactoryGirl.build(:title); end

取り急ぎ Guard の結果は以下のようになり、テストが通過しています。

Title
  妥当なオブジェクトが生成されること

Finished in 0.01572 seconds
1 example, 0 failures

しかしながら、データベースのテーブルには not null 制約を付けているので、Rails 側でも validation をする必要があります。そのため、yomi_suuji を除いた 3 つの属性が空欄の時にエラーとなるかどうかを確認します。

  [ :japanese, :english, :yomi ].each do |key|
    it "#{key} は空であってはならない" do
      title[key] = ""
      expect(title).not_to be_valid
      expect(title.errors[key]).to be_present
    end
  end

Guard の結果は以下のようになります。本来エラーになるべきのものが、成功しているためにテストとしてはエラーとなります。

Failures:

  1) Title english は空であってはならない
     Failure/Error: expect(title).not_to be_valid
       expected #<Title id: nil, japanese: "日本語 英語", english: "", yomi: "にほんご えいご", yomi_suuji: "ダミーデータ, created_at: nil, updated_at: nil> not to be valid
     # ./spec/models/title_spec.rb:13:in `block (3 levels) in <top (required)>'
     # -e:1:in `<main>'

  2) Title japanese は空であってはならない
     Failure/Error: expect(title).not_to be_valid
       expected #<Title id: nil, japanese: "", english: "English Japanese", yomi: "にほんご えいご", yomi_suuji: "ダミーデータ", created_at: nil, updated_at: nil> not to be valid
     # ./spec/models/title_spec.rb:13:in `block (3 levels) in <top (required)>'
     # -e:1:in `<main>'

  3) Title yomi は空であってはならない
     Failure/Error: expect(title).not_to be_valid
       expected #<Title id: nil, japanese: "日本語 英語", english: "English Japanese", yomi: "", yomi_suuji: "ダミーデータ", created_at: nil, updated_at: nil> not to be valid
     # ./spec/models/title_spec.rb:13:in `block (3 levels) in <top (required)>'
     # -e:1:in `<main>'

Finished in 0.02257 seconds
4 examples, 3 failures

Failed examples:

rspec ./spec/models/title_spec.rb:11 # Title english は空であってはならない
rspec ./spec/models/title_spec.rb:11 # Title japanese は空であってはならない
rspec ./spec/models/title_spec.rb:11 # Title yomi は空であってはならない

テストを通過させるために、app/models/title.rb に validation を追加します。

class Title < ActiveRecord::Base
  validates :japanese, :english, :yomi, :yomi_suuji, presence:true
end

Guard の結果は以下のようになります。テストは全て成功しています。

Title
  妥当なオブジェクトが生成されること
  english は空であってはならない
  japanese は空であってはならない
  yomi は空であってはならない

Finished in 0.04914 seconds
4 examples, 0 failures

yomi_suuji の自動生成

現在、yomi_suuji にはダミーデータを入れています。本来、yomi_suuji は自動設定されるべきものなので、FactoryGirl のダミーデータを削除して "" だけにしてみます。

FactoryGirl.define do
  factory :title do
    japanese "日本語 英語"
    english "English Japanese"
    yomi "にほんご えいご"
    yomi_suuji ""
  end
end

Guard の結果は以下のようになります。yomi_suuji が空のためにエラーになってしまいました。

Failures:

  1) Title 妥当なオブジェクトが生成されること
     Failure/Error: expect(title).to be_valid
       expected #<Title id: nil, japanese: "日本語 英語", english: "English Japanese", yomi: "にほんご えいご", yomi_suuji: "", created_at: nil, updated_at: nil> to be valid, but got errors: Yomi suujiを入力してください。
     # ./spec/models/title_spec.rb:7:in `block (2 levels) in <top (required)>'
     # -e:1:in `<main>'

Finished in 0.05041 seconds
4 examples, 1 failure

yomi_suuji の validation でエラーとなっているので、yomi_suuji は validation 前に実行する必要があります。このため、title クラスに before_validation を設定します。ここでは yomi の文字列を空白で区切って、それぞれを yomi_suuji に変換した後、「-」で結合することで、仕様を満たす文字列が生成されることになります。

class Title < ActiveRecord::Base
  include YomiSuuji
  validates :japanese, :english, :yomi, :yomi_suuji, presence:true
  before_validation do
    self.yomi_suuji = self.yomi.split(' ').map { |str| convert_yomi_suuji(str) }.join('-')
  end
end

肝心の読み数字を生成する convert_yomi_suuji ですが、これまでにもいろんなアプリで使っており、Module 化されているのでそれを利用します。以前は、lib などのフォルダに関連モジュールなどを配置していましたが、Rails4 の場合には concerns フォルダに配置するようになりました。今回は以下のような app/models/concerns/yomi_suuji.rb を記載します。以前は、1文字ずつのマッピングテーブルを用意していましたが、行と列のデータを後から zip すればよいことに気づき、tr だけで簡潔に記述するように変わっています。

module YomiSuuji
  Kana = [*"ぁ".."&#12438;"].join("") + "ー・"

  def yomi_suuji_sub(str)
    first = (str.tr Kana, "0000000000111111111122222222223333333333344444555555555555555666667777778888899999951199").split(//)
    second = (str.tr Kana, "0011223344001122334400112233440011222334401234000111222333444012340022440123400134520399").split(//)
    last = str.tr Kana, "0000000000010101010101010101010101001010100000012012012012012000000000000000000000010000"
    [ first.zip(second).flatten.join(""), last].join('.')
  end
end

Guard の結果は以下のようになります。yomi_suuji が設定されて、「妥当なオブジェクトが生成されること」が成功になっています。

Title
  妥当なオブジェクトが生成されること
  yomi は空であってはならない
  japanese は空であってはならない
  english は空であってはならない

Finished in 0.04692 seconds
4 examples, 0 failures

正しく、yomi_suuji が入っているかが心配なので、yomi_suuji の文字列もテストします。validation が発生しないと yomi_suuji が設定されないので、validation check を行っています。

  it "yomi_suuji が正しく設定できること" do
    title.valid?
    expect(title.yomi_suuji).to eq('41549514.0001-030114.001')
  end

Guard の結果はこうなりました。

Title
  妥当なオブジェクトが生成されること
  japanese は空であってはならない
  yomi は空であってはならない
  english は空であってはならない
  yomi_suuji が正しく設定できること

Finished in 0.05624 seconds
5 examples, 0 failures

せっかくなので、全文字が正しく変換できているかを確認してみます。

    title.yomi = [*"ぁ".."&#12438;"].join(" ") + " ー・"
    title.valid?
    expect(title.yomi_suuji).to eq('00.0-00.0-01.0-01.0-02.0-02.0-03.0-03.0-04.0-04.0-10.0-10.1-11.0-11.1-12.0-12.1-13.0-13.1-14.0-14.1-20.0-20.1-21.0-21.1-22.0-22.1-23.0-23.1-24.0-24.1-30.0-30.1-31.0-31.1-32.0-32.0-32.1-33.0-33.1-34.0-34.1-40.0-41.0-42.0-43.0-44.0-50.0-50.1-50.2-51.0-51.1-51.2-52.0-52.1-52.2-53.0-53.1-53.2-54.0-54.1-54.2-60.0-61.0-62.0-63.0-64.0-70.0-70.0-72.0-72.0-74.0-74.0-80.0-81.0-82.0-83.0-84.0-90.0-90.0-91.0-93.0-94.0-95.0-52.1-10.0-13.0-9999.00')

すべての文字が変換できていることが確認できました。

読みの検査

読みに正しくひらがなのみが設定されていれば問題がないのですが、そうでない場合には、読み数字に変な文字列が設定されてしまいます。設定された文字列以外のものが読みに設定された時にエラーが出るように設定します。その確認のためにテストを追加します。

  it "読みにひらがなと設定された記号以外の文字が設定されていた場合にエラーとなること" do
    title.yomi = "日本語"
    expect(title).not_to be_valid
    expect(title.errors[:yomi]).to be_present
  end

Guard の結果は以下のようになります。

Failures:

  1) Title 読みにひらがなと設定された記号以外の文字が設定されていた場合にエラーとなること
     Failure/Error: expect(title).not_to be_valid
       expected #<Title id: nil, japanese: "日本語 英語", english: "English Japanese", yomi: "日本語", yomi_suuji: "日日本本語語.日本語", created_at: nil, updated_at: nil> not to be valid
     # ./spec/models/title_spec.rb:28:in `block (2 levels) in <top (required)>'
     # -e:1:in `<main>'

Finished in 0.07446 seconds
6 examples, 1 failure

Failed examples:

yomi_suuji になかなか楽しい文字列が設定されていることがわかります。この場合にエラーになるように app/models/title.rb に validation を追加します。

  validates :yomi, format: { with: /\A[\p{Hiragana}ー・ ]+\z/, allow_blank: true } 

この結果、Guard では、6 つのテストが無事に成功したことになります。

Title
  妥当なオブジェクトが生成されること
  yomi は空であってはならない
  english は空であってはならない
  japanese は空であってはならない
  yomi_suuji が正しく設定できること
  読みにひらがなと設定された記号以外の文字が設定されていた場合にエラーとなること

Finished in 0.06541 seconds
6 examples, 0 failures

とりあえず、Title モデルについては今ところはここまでとなります。

written by iHatenaSync