yucatio@システムエンジニア

趣味で作ったものいろいろ

Railsで変更前の(DBに保存されている)値を利用してvaldiateする

Webアプリでvalidationを書いていると、DBに保存されている値が必要になる場合があります。Railsでは、DBに保存されている値は、#{attr_name}_in_database、保存されている値から変更があったかは、will_save_change_to_#{attr_name}?というメソッドが用意されています。#{attr_name}の部分はModelの属性(カラム名)で置き換えます。

変更前の値を利用してvaldiateする

前回に引き続き、会議室予約アプリを例にして、具体的な使い方をみてみます。

会議室(meeting_room)は複数の会議予約(meeting)をもちます。会議予約は、開始時刻(start_time)と終了時刻(end_time)をもちます。

f:id:yucatio:20180809214216p:plain

★前回の記事

yucatio.hatenablog.com

仕様

  • 会議予約(前回実装済み)
    • subjectは32文字以下で必須項目
    • start_timeは必須項目
    • end_timeは必須項目
    • end_timeはstart_timeより後であること
    • 新規登録時、start_timeは現在時刻より後であること
    • start_timeが変更された時、start_timeは現在時刻より後であること

これに、

  • 会議予約
    • 保存済みのstart_timeが現在より前の場合は、start_timeを変更できない
    • 保存済みのstart_timeが現在より前の場合は、end_timeを変更できない

この2つのvalidationを追加します。

コード

class Meeting < ApplicationRecord
  # 略

  validate :start_time_cannot_be_changed
  validate :end_time_cannot_be_changed

  private
  # 略

  def after_meeting_start?
    start_time.present? && !new_record? && start_time_in_database <= Time.zone.now
  end

  def start_time_cannot_be_changed
    if after_meeting_start?
      if will_save_change_to_start_time?
        errors.add(:start_time, 'は変更できません。会議開始時刻を過ぎています')
      end
    end
  end

  def end_time_cannot_be_changed
    if after_meeting_start?
      if will_save_change_to_end_time?
        errors.add(:end_time, 'は変更できません。会議開始時刻を過ぎています')
      end
    end
  end
end

after_meeting_start?メソッド内の start_time_in_databaseでは、DBに保存されている(つまり更新前の)start_timeの値を取得できます。 start_time.present? && !new_record? && start_time_in_database <= Time.zone.now は、start_timeがnilでなく、新規レコードでなく(≒更新で)、DBに保存されているstart_timeが、現在時刻より前である、という意味です。

start_time_cannot_be_changedメソッド内でwill_save_change_to_start_time?を使用して、値が変更された時にエラーを出すようになっています。end_time_cannot_be_changedメソッドも同様です。

実行結果

更新時

※実行時は2018年8月10日

開始時間が過去で登録されていて、日時が変更無しのとき(subjectのみ変更)は更新できる↓

f:id:yucatio:20180811154046p:plain

開始時間が過去の場合、他の日時へは変更できない↓

f:id:yucatio:20180811222525p:plain

開始時間が未来の場合、別の未来の日時には変更できる↓

f:id:yucatio:20180811161128p:plain

環境

続く

こちらもどうぞ

yucatio.hatenablog.com

参考

Rails4とRails5ではDBの値をとるメソッドや変更されたかを取得するメソッドの名前が変更されています。rail5の方がわかりやすいメソッド名ですね。#{attr_name}_in_databasewill_save_change_to_#{attr_name}? 以外のメソッドもこちらにまとまっています。

qiita.com

Railsのwill_save_change_toで値が変更されているか調べてvaldiateする

Webアプリでvalidationを書いていると、値が変更になった時にだけvalidationをかけたい場合があります。Railsではwill_save_change_to_#{attr_name}?というメソッドが用意されています。#{attr_name}の部分はModelの属性(カラム名)で置き換えます。

値が変更されているか調べてからvaldiateする

会議室予約アプリを例にして、具体的な使い方をみてみます。

会議室(meeting_room)は複数の会議予約(meeting)をもちます。会議予約は、開始時刻(start_time)と終了時刻(end_time)をもちます。

f:id:yucatio:20180809214216p:plain

ルーティングはこちらです。

# routes.rb
Rails.application.routes.draw do
  resources :meeting_rooms do
    resources :meetings
  end
end

仕様

  • 会議予約
    • subjectは32文字以下で必須項目
    • start_timeは必須項目
    • end_timeは必須項目
    • start_timeはend_timeより前であること
    • 新規登録時、start_timeは現在時刻より後であること
    • start_timeが変更された時、start_timeは現在時刻より後であること

コード

本題ではない方のvalidation
  • 会議予約
    • subjectは32文字以下で必須項目
    • start_timeは必須項目
    • end_timeは必須項目
    • start_timeはend_timeより前であること

このへんは今回の本題ではないのでコードだけ載せておきます

class Meeting < ApplicationRecord
  belongs_to :meeting_room

  validates :subject, length: {maximum: 32}, presence: true
  validates :start_time, presence: true
  validates :end_time, presence: true

  validate :start_time_should_be_before_end_time

  private
  def start_time_should_be_before_end_time
    return unless start_time && end_time

    if start_time >= end_time
      errors.add(:start_time, 'は終了時間よりも前に設定してください')
    end
  end
end

値の変更時にvalidationをかける

  • 会議予約
    • 新規登録時、start_timeは現在時刻より後であること
    • start_timeが変更された時、start_timeは現在時刻より後であること

こちらを実装したコードは以下です。

class Meeting < ApplicationRecord
  # 略

  validate :start_time_should_be_after_current, if: :new_or_start_time_changed

  private
  # 略

  def new_or_start_time_changed
    start_time.present? && (new_record? || will_save_change_to_start_time?)
  end

  def start_time_should_be_after_current
    if start_time <= Time.zone.now
      errors.add(:start_time, 'は現在時刻よりも後に設定してください')
    end
  end
end

new_or_start_time_changedメソッドの中で、 will_save_change_to_start_time?を使うことによって、start_timeが変更されたかどうかを判定しています。

start_time.present? && (new_record? || will_save_change_to_start_time?)は、start_timeがnilでないかつ、新規またはstart_timeの値が変更されている、という意味です。

if: :new_or_start_time_changedを外して、

class Meeting < ApplicationRecord
  # 略

  validate :start_time_should_be_after_current

  private
  # 略

  def start_time_should_be_after_current
    if start_time <= Time.zone.now
      errors.add(:start_time, 'は現在時刻よりも後に設定してください')
    end
  end
end

と書いてしまうと、start_timeが過去になってしまうと、全ての項目(subjectなど)が更新できなくなってしまうので注意してください。

実行結果

新規登録時

※実行時は2018年8月10日

開始時間に過去の日時は登録できない↓

f:id:yucatio:20180811152352p:plain

開始時間に未来の日時は登録できる↓

f:id:yucatio:20180811152403p:plain

更新時

※実行時は2018年8月10日

開始時間が過去で登録されていて、日時が変更無しのとき(subjectのみ変更)は更新できる↓

f:id:yucatio:20180811154046p:plain

開始時間が過去の場合、未来の日時へ変更できる↓

f:id:yucatio:20180811154934p:plain

開始時間が過去の場合、別の過去の日時には変更できない↓

f:id:yucatio:20180811155634p:plain

開始時間が未来の場合、 過去の日時には変更できない↓

f:id:yucatio:20180811160509p:plain

開始時間が未来の場合、別の未来の日時には変更できる↓

f:id:yucatio:20180811161128p:plain

環境

続く

こちらもどうぞ

yucatio.hatenablog.com

yucatio.hatenablog.com

参考

Rails4とRails5ではDBの値をとるメソッドや変更されたかを取得するメソッドの名前が変更されています。rail5の方がわかりやすいメソッド名ですね。#{attr_name}_in_databasewill_save_change_to_#{attr_name}? 以外のメソッドもこちらにまとまっています。

qiita.com

Rubyで全角空白も対象としたstrip(trim)

rubyではtrimじゃなくてstripというそうです。 stripは先頭と末尾の半角空白やタブ、改行などを除去するメソッドです。

全角空白は除去されないので、独自で定義する必要があります。

うまくいく例

class String
  def strip_all_space!
    gsub!(/(^[[:space:]]+)|([[:space:]]+$)/, '')
  end

  def strip_all_space
    self_clone = clone
    self_clone.strip_all_space!
    self_clone
  end
end
puts '   前後空白 全角半角混合   '.strip_with_2byte_space
# => '前後空白 全角半角混合'

str = '   前後空白 全角半角混合   '
str.strip_with_2byte_space!
puts str
# => '前後空白 全角半角混合'

仕様

strip_all_spaceメソッドは、文字列の先頭と末尾の空白文字を除去した新しい文字列を返します。 空白文字は、正規表現の[:space:]の文字と同じで、全角空白などユニコードの空白文字も含まれています。

strip_all_space!メソッドは、レシーバ自身の文字列から先頭と末尾の空白文字を除去します。変更があった場合はレシーバ自身を、変更がない場合はnilを返します。

正規表現

gsub!(/(^[[:space:]]+)|([[:space:]]+$)/, '')

こちらのメソッド呼び出しですが、まず gsub!は、正規表現でマッチした部分を繰り返し置換するメソッドです。

マッチさせる正規表現(^[[:space:]]+)|([[:space:]]+$)の前半の(^[[:space:]]+)は、「文字列の先頭に続き、[:space:]クラスの1文字以上の繰り返し」を表します。 後半の([[:space:]]+$)は、「[:space:]クラスの1文字以上の繰り返し、その直後に終端(行末)」を表します。その2つを|で結合することによって、そのどちらにもマッチするようになります。

マッチした部分を空文字に置き換えることにより、行頭と行末の空白文字を除去しています。

strip_all_space(非破壊的メソッド)からstrip_all_space!(破壊的メソッド)を呼び出す

def strip_all_space
  self_clone = clone
  self_clone.strip_all_space!
  self_clone
end

cloneで自身のコピーを作成し、コピーした方に破壊的変更をすることで、自身の情報には変更を加えず、またコードの重複なく目的のメソッドを作成できました。

Stringクラスの破壊的メソッドの戻り値

strip!sub!gsub!tr!などのStringクラスに定義されている破壊的メソッドの戻り値は、 変更があった場合はレシーバ自身を、変更がない場合はnilを返します。

変更がないときにnilを返すのは少し奇妙に思いましたが、これは、変更があった時(もしくはなかった時)に処理をするためだと思います。

str = ' test '
if str.strip!
  # 変更があったとき
  puts '文字列が破壊的に変更されました'
else
  # 変更がなかったとき
  puts '文字列は変更されませんでした'
end

独自にStringクラスを拡張する場合もこの規則に従い、破壊的変更をする場合は、変更があった場合はレシーバ自身を、変更がない場合はnilを返すようにするべきと考えます。

誤った例

全角空白も対象としたstripはweb上にいくつか情報が存在し、参考にさせてもらったのですが、いくつか求める仕様と異なっていることに気づかず使おうとしてしまったものもありました。同じ間違いをする人が出ないように、例とどの場合にうまくいかないかを示します。

誤った例 (その1)

def strip_all_space!
  sub!(/^[[:space:]]*([^[:space:]]+)[[:space:]]*$/, '\1')
end

行頭に続き、[:space:]クラスの0回以上の繰り返し、[:space:]クラスでない文字の1回以上の繰り返し、[:space:]クラスの0回以上の繰り返し、行末、にマッチさせ、中間の部分を抜き出す表現です。

うまくいかない場合ですが、

まず、' 'のように空白のみで構成される文字列にマッチしません。 また、' 文 字 列 'のように空白でない文字と文字の間に空白が入る文字列にもマッチせず、前後の空白が除去されません。

誤った例 (その2)

def strip_all_space!
   gsub!(/^[[:space:]]*(.*?)[[:space:]]*$/, '\1')
end

行頭に続き、[:space:]クラスの0回以上の繰り返し、任意の文字列の0回以上の繰り返し、[:space:]クラスの0回以上の繰り返し、行末、にマッチさせ、中間の部分を抜き出す表現です。(.*?)の部分で最短マッチを行なっています。

' 文 字 列 'のように空白でない文字と文字の間に空白が入る文字列も問題なく'文 字 列'のように前後の空白が除去されます。

ただ、'文字列'のように変更がない文字列にも正規表現がマッチしてしまい、「変更がない場合はnilを返す」という仕様に合いませんでした。

誤った例 (その3)

class String
  def strip_all_space!
    gsub!(/(^[[:space:]]+)|([[:space:]]+$)/, '')
  end

  def strip_all_space
    clone.strip_all_space!
  end
end

非破壊的メソッドからcloneメソッドを使用して破壊的メソッドを呼ぶ方法をwebで発見したのですが、cloneに対して破壊的メソッドを呼び、戻り値をそのまま返しています。

strip_all_space!メソッドは変更がないときにnilを返すので、戻り値をそのまま返すとバグになります。cloneを一旦別の変数に保存し、破壊的メソッドを呼び出した後に、cloneしたものを返す必要があります。


Railsでの標準ライブラリの拡張方法

Stringを拡張する際のrailsでの記述方法はこちらが参考になりました。 ただし、この方法では、起動時にしかStringクラスが読み込まれず、書き換えた場合に再起動が必要という問題があります。

qiita.com

テストケース

簡単ですが、rspecでテストを書きました。正規表現のテストケース書くの難しい。

describe 'Strip all space behavior' do
  describe '#strip_all_space!' do
    subject { str.strip_all_space! }

    context 'When all space string' do
      let(:str) { '  ' }
      it { is_expected.to eq ''}
      it 'should remove spaces' do
        subject
        expect(str).to eq ''
      end
    end

    context 'When all space string including 2-byte space' do
      let(:str) { '   ' }
      it { is_expected.to eq ''}
      it 'should be empty string' do
        subject
        expect(str).to eq ''
      end
    end

    context 'When space string including 2-byte space at start of string' do
      let(:str) { '   文字列' }
      it { is_expected.to eq '文字列'}
      it 'should remove spaces' do
        subject
        expect(str).to eq '文字列'
      end
    end

    context 'When space string including 2-byte space at end of string' do
      let(:str) { '文字列    ' }
      it { is_expected.to eq '文字列'}
      it 'should remove spaces' do
        subject
        expect(str).to eq '文字列'
      end
    end

    context 'When space string including 2-byte space around string' do
      let(:str) { '    文字列    ' }
      it { is_expected.to eq '文字列'}
      it 'should remove spaces on start/end of string' do
        subject
        expect(str).to eq '文字列'
      end
    end

    context 'When space around string including white space' do
      let(:str) { '    文 字 列    ' }
      it { is_expected.to eq '文 字 列'}
      it 'should remove start/end spaces. Spaces between string are preserved' do
        subject
        expect(str).to eq '文 字 列'
      end
    end

    context 'When empty string' do
      let(:str) { '' }
      it { is_expected.to eq nil}
      it 'should not be changed' do
        subject
        expect(str).to eq ''
      end
    end

    context 'When string does NOT have any white space' do
      let(:str) { '文字列' }
      it { is_expected.to eq nil}
      it 'should not be changed' do
        subject
        expect(str).to eq '文字列'
      end
    end
  end

  describe '#strip_all_space' do
    subject { str.strip_all_space }

    context 'When all space string' do
      let(:str) { '  ' }
      it { is_expected.to eq ''}
      it 'should be original string' do
        subject
        expect(str).to eq '  '
      end
    end

    context 'When all space string including 2-byte space' do
      let(:str) { '   ' }
      it { is_expected.to eq ''}
      it 'should be original string' do
        subject
        expect(str).to eq '   '
      end
    end

    context 'When space string including 2-byte space at start of string' do
      let(:str) { '   文字列' }
      it { is_expected.to eq '文字列'}
      it 'should be original string' do
        subject
        expect(str).to eq '   文字列'
      end
    end

    context 'When space string including 2-byte space at end of string' do
      let(:str) { '文字列    ' }
      it { is_expected.to eq '文字列'}
      it 'should be original string' do
        subject
        expect(str).to eq '文字列    '
      end
    end

    context 'When space string including 2-byte space around string' do
      let(:str) { '    文字列    ' }
      it { is_expected.to eq '文字列'}
      it 'should be original string' do
        subject
        expect(str).to eq '    文字列    '
      end
    end

    context 'When space around string including white space' do
      let(:str) { '    文 字 列    ' }
      it { is_expected.to eq '文 字 列'}
      it 'should be original string' do
        subject
        expect(str).to eq '    文 字 列    '
      end
    end

    context 'When empty string' do
      let(:str) { '' }
      it { is_expected.to eq ''}
      it 'should be original string' do
        subject
        expect(str).to eq ''
      end
    end

    context 'When string does NOT have any white space' do
      let(:str) { '文字列' }
      it { is_expected.to eq '文字列'}
      it 'should be original string' do
        subject
        expect(str).to eq '文字列'
      end
    end
  end
end
環境

「オブジェクト指向とは何か」の答えが『なぜ、あなたの仕事は終わらないのか(中島聡[著])』に書いてあったので紹介します

オブジェクト指向とは

『なぜ、あなたの仕事は終わらないのか(中島聡[著])』には以下のように書いてあります。

なんらかの対象(オブジェクト)を先に選択したうえで動作を指定することをオブジェクト指向といいます。

考えてみる

Wikipediaオブジェクト指向を調べてみると、

オブジェクト指向 - Wikipedia

オブジェクト指向(オブジェクトしこう)とは、オブジェクト同士の相互作用として、システムの振る舞いをとらえる考え方である。

とあります。これに異論はありません。

ただ、Wikipediaの文章は、オブジェクト指向がわかる人であれば理解できる文ですが、初心者にとっては抽象的すぎて何を言ってるのか捉えにくいかもしれません。

冒頭で引用した説明もこれだけでは何を言っているのかよくわからないかも知れませんが、 書籍中では、 Windows95での右クリックやダブルクリック、ドラック&ドロップが現在のようになった、という話の中で出てきており、理解がしやすいようになっていました。 以下に例を示します。

オブジェクト指向GUI

WindowsののGUIでは、 ファイルの上で右クリックすると、選択できる動作が表示されます。

f:id:yucatio:20180801122931p:plain

また、ダブルクリックで、文書ファイルならMicrosoft Wordが起動し、音楽ファイルなら音楽プレーヤーが起動します。

注目すべきは、動作(ファイルを開く、ファイルを削除する)を始めに指定するのではなく、ファイルを先に指定しているという点です。

オブジェクト指向以前は動作が先

オブジェクト指向以前では、ユーザがコマンドという文字列を最初に入力してそのあとにファイルやディレクトリを指定していました。

例えばエクセルファイルを開くのにはexcel.exeコマンドを使用します。

excel.exe 集計.xls

ファイルの削除はdelコマンドを使用します。(delはdeleteの略)

del 集計.xls

扱う言語(自然言語)による違い

書籍には続いて以下のように書いてあります。

オブジェクト指向のわかりやすい例として、私たちがいつも使っている日本語が挙げられます。

あなたがテーブルの上の塩を取ってほしいとき、あなたは「すいません、塩を……」まで言葉にしたところで一呼吸置くと思います。「塩」という対象を指定した時点で、あなたが相手にしてほしいことは決まり切っているからです。

(中略)

これはWindowsにおけるダブルクリックと非常によく似ていると思いませんか?このようなグラフィカルでオブジェクト指向な機能を思いつくことができたのは、もしかしたら私が当時マイクロソフトで唯一の日本語話者だったからかもしれません。

これは面白い考察でした。確かに英語の命令文だとはじめに動詞が来ます。なのでコンピュータに対する命令を入力するときに、始めにコマンド(動詞であることが多い)、そのあとに対象ファイルを指定するのが自然な考えでしょう。

日本語がオブジェクト指向な言語かどうかよくわかりません。目的語が最初というより、動詞が最後にくるだけな気がしますがどうなのだろう。

ところで、オブジェクト指向において、メソッド呼び出しを「メッセージ」と呼ぶのは、オブジェクト指向のプログラミングにおいて、オブジェクト.命令の部分を英語的に読みくだしたい場合に、オブジェクトに対して呼びかけている様子をイメージさせるためかと思いました。

# オブジェクト指向的にファイルを開くコード
fileA.open()

# 英語的に読みくだしてみる(適当)
# Hey fileA! Open!

まとめ

オブジェクト指向がよくわからない、と思ったときには、 windowsのファイルアイコンの操作のように、「先に操作の対象(ファイルやディレクトリ)を決めて、次に動作(開く、移動する、削除する、など)を決める」 というイメージを持つとよいかと思います。

実際、JavaIDEでは、対象を指定した段階で、呼び出す動作(メソッド)の候補が表示されます。

f:id:yucatio:20180801115942p:plain

(オブジェクト指向をよくわかっている人の場合にはこのイメージだけでは物足りないと思いますが。カプセル化とかポリモーフィズムとか)。

書籍の紹介

引用した書籍はプログラミングの本ではなく、仕事術の本ですが、著者がマイクロソフトWindows95の開発をしていた頃のエピソードも盛り込まれています。そのエピソードがエキサイティングなので読み物としてもとても楽しめました。


引用した文章はこちらで読めます

www.huffingtonpost.jp

あとがき

普段オブジェクト指向でプログラミングをしていて、オブジェクト指向の利点を利用したプログラムを書いていますが、いざ「オブジェクト指向とは何か言葉で説明しなさい」ともし言われたとしたら答えられない、ということに気づきました。

この記事を書くにあたり、オブジェクト指向とは何かというのをwebで検索しました。書籍に書かれていたような定義をしているサイトがなく(そもそもサイトごとに定義が違う)、"書籍の中で使用している「オブジェクト指向」と私が思っている「オブジェクト指向」は別物なのでは"という疑念も湧きましたが、

オブジェクト指向と20年戦ってわかったこと

ここの記事に書いてあった、

言語ごとの多少の表記法の違いはあれど、「最大公約数」を取り出そうとしたら、たぶん

オブジェクト.メソッド(引数...)

という部分しか残らないのでは、と思います。

という記述を見つけ、安心しました。

複数行の式とRubyMineのフォーマッタとの相性が悪いのでIssueをあげました

こちらの記事を書いてる最中に、複数行の式とRubyMineのフォーマッタの相性が悪いと気づきました。

yucatio.hatenablog.com

算術オペレータを最後に置いて改行する場合

算術オペレータを最後に置く方法だと、フォーマットをかけた際に、RubyMineは4個のスペースを入れるのに対し、RuboCopは、オペランド(=足されている数)を揃えるようにと警告が出ます。

フォーマットかける前↓(オペランドが揃っていて、RuboCopの警告は出ない)

f:id:yucatio:20180628123856p:plain

フォーマッタをかけた後↓(インデントが崩れ、RuboCopの警告が出る)

f:id:yucatio:20180628123413p:plain

バックスラッシュを用いて改行する場合

一方、バックスラッシュを用いる場合は、フォーマットをかけるたびに続く行がどんどん後ろへとずれていく現象が起こります。

フォーマットかける前↓

f:id:yucatio:20180628125116p:plain

フォーマット1回かけた後↓

f:id:yucatio:20180628125151p:plain

さらに4回フォーマットをかけたとき↓

f:id:yucatio:20180628125414p:plain

さらに5回フォーマットをかけたとき↓

f:id:yucatio:20180628125435p:plain

全てのバックスラッシュを用いた改行でこの現象が起こるわけではなく、ある特定の条件の場合のみのようですが、どのような条件で起きるかは定かではありません。

Issueをあげておきました

JetBrainsのバグトラックシステムにissueをあげました。

Reformat CodeするとRubocopの警告でる : https://youtrack.jetbrains.com/issue/RUBY-21925

Reformat Codeのたびに空白が足される : https://youtrack.jetbrains.com/issue/RUBY-21927

環境

Rubyでは、式の途中で改行するときは、算術オペレータを一番後ろに置いてから改行する

Javaの人間がRubyでやってしまった失敗を1つ紹介します。

意図した通りに動かないコード

こんなコードを書きました

sum = 100 + 200
        + 300 + 400
puts sum
#=> 300

100+200+300+400 (=10000)のつもりで書いていましたが、最初の2つしか足されていません。

デバッグ

Rubyでは、原則として行の終わり=式の終わりなので、明示的に行が続くことを示さなければいけません。

算術オペレータを最後に置く

明らかに式が続いていると判断される場合には、次の行も続いて評価されるので、算術オペレータを後ろに置けば続く行も1つの式として評価されます。

sum = 100 + 200 +
      300 + 400
puts sum
#=> 1000

バックスラッシュを使う

バックスラッシュを使うと、明示的に式が続いていることを示せます。

sum = 100 + 200 \
      + 300 + 400
puts sum
#=> 1000

入門書にちゃんと書いてあった

『初めてのRuby』(Yugui[著])を読み返してたらちゃんと書いてありました。

次のように記述すると1と-2という別個の式が並んでいるものと解釈されます。(中略)

1
   - 2

括弧を用いる方法はうまくいかない

ちなみに以下のように、式全体を括弧でくくる方法はうまくいきません。

sum = (100 + 200
       + 300 + 400)
puts sum
#=> 700

これは、 Rubyの改行 - 永遠に未完成 のコメントで言及されているように、

sum = (100 + 200
       + 300 + 400)

は、

sum = (100 + 200;
       + 300 + 400)

と解釈され、(100 + 200; + 300 + 400)(+ 300 + 400)と等価になるからです。

Pythonだと式全体を括弧でくくるとうまくいくので、Rubyでもそうかと思いましたが、違いました。

[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

初めてのRuby [ Yugui ]
価格:2376円(税込、送料無料) (2018/6/23時点)


複数行の式とRubyMineのフォーマッタとの相性が悪い

複数行にまたがって式を書く方法は、現在、RubyMineのフォーマッタ(Code > Reformat Code)との相性がよくないです。(RubyMine version: 2018.1.3)

yucatio.hatenablog.com

環境

Rubyでは、メソッド引数のカッコはメソッド名の直後に書くこと。空白を入れちゃだめ

引き続き、『プロを目指す人のためのRuby入門』(伊藤淳一[著])を読んでいます。

問題発生

本の中のコードを(間違って)写して、下記を実行したところ、

class DeepFreezableTest < Minitest::Test
  def test_deep_freeze_to_hash
    # ハッシュの値は正しいか
    assert_equal (
        {'Japan' => 'yen', 'US' => 'dollar', 'India' => 'rupee'},
        Bank::CURRENCIES
    )
  end
end
syntax error, unexpected ',', expecting ')' (SyntaxError)

ハッシュの直後にあるカンマで文法エラーが発生しました。

解決方法

assert_equal(の間の空白(半角スペース)を削除する

class DeepFreezableTest < Minitest::Test
  def test_deep_freeze_to_hash
    # ハッシュの値は正しいか
    assert_equal(
        {'Japan' => 'yen', 'US' => 'dollar', 'India' => 'rupee'},
        Bank::CURRENCIES
    )
  end
end

なぜエラーが発生したのか

assert_equalとメソッド引数のカッコに半角スペースを入れたことにより、({'Japan' => 'yen', 'US' => 'dollar', 'India' => 'rupee'}, Bank::CURRENCIES)全体が1つ目の引数と解釈されてしまいました。どうやら、メソッド名の後に空白があるとメソッド呼び出しのカッコが省略されたと解釈されるらしいです。

{'Japan' => 'yen', 'US' => 'dollar', 'India' => 'rupee'}, Bank::CURRENCIES自体は式として解釈されないので、文法エラーになりました。

あとがき

空白の有無でSyntaxErrorになってしまうのは予想外でした。 エラーが出た時にtypoかと思って本と見比べるも、空白の有無は見逃してしまい、解決に時間がかかってしまいました。 RubyMineでこの文法エラーとしては表示されませんでした。よく見たら警告出てました。見逃してました。

f:id:yucatio:20180617171129p:plain

googleでエラーメッセージを検索しましたが、なにせ記号が入っているため、別のエラーメッセージが検索結果に出てしまい、なかなか同じエラーメッセージが出ているページにたどり着けなかったです。

色々試した結果、解決してよかったです。どんなエディタでも空白文字を表示しておくのはおすすめです。

yucatio.hatenablog.com

補足

念のため書いておきますが、本に書いてあるコードを間違わずに写せばエラーは出ません。

環境