経緯
rubyを1.8.7から1.9.3にバージョン上げる時にTime.parseの引数に空文字列を指定すると例外が発生するようになっていました。
[EXCEPTION] ArgumentError:
dateに空文字列を与えた場合、発生します。 なお、1.9.2より前は例外は発生せず、現在時刻を表す Time のインスタンスを返していました。
Ruby 1.9.2 リファレンスマニュアル Time.parse
Time.parseで空文字を指定している箇所が多すぎて修正が大変であったため、Time.parseにモンキーパッチを当てて解決することにしました。
結論
空文字列が指定されたらTime.nowを返して、それ以外は組み込みクラスのparseメソッドを呼ぶようなオーバライド的な処理にしました。
class Time
class << self
alias_method :__parse__, :parse
private :__parse__
def parse(time)
if time.empty?
# 現時刻を返す
Time.now
else
# 組み込みクラスTime.parseを呼ぶ
__parse__(time)
end
end
end
end
self.parseで定義しても可能です。
class Time
class << self
alias_method :__parse__, :parse
private :__parse__
end
def self.parse(time)
if time.empty?
# 現時刻を返す
Time.now
else
# 組み込みクラスTime.parseを呼ぶ
__parse__(time)
end
end
end
結論に至るまで
Timeのオーバライド
組み込みクラスのTimeを活かしつつ空文字列のチェックをしたかったので、Timeを継承してオーバライドすれば良いかなと思いました。
ですが、継承する時にクラス名を別名にして、Time.parse(arg)が使われている箇所をすべて新しいクラス名に変更する必要があります。
これでは本末転倒なので断念しました。
Time.parseのモンキーパッチ
次にモンキーパッチを検討しました。
クックパッドさんでもモンキーパッチの対応があったため、こちらを参考にさせていただきました。(なるべくやるなと書いてありましたが。。)
Ruby on Rails アプリケーションにおけるモンキーパッチの当て方
Timeの拡張クラスを用意して以下の様に定義しました。
※拡張クラスの作り方についてはクラスの拡張の記事を参照させていただきました。
class Time
def self.parse(time)
if time.empty?
Time.now
else
Time.parse(time)
end
end
end
ですが、これではエラー。
拡張クラスのTime.parseがずっと呼ばれ無限ループ状況になってしまったので、これもNG。
irb(main):002:0> Time.parse('2020-01-01 12:34:56')
SystemStackError: stack level too deep
from /usr/local/lib/ruby/1.9.1/irb/workspace.rb:80
Maybe IRB bug!
Time.parseのモンキーパッチ & alias_methodの適用
先程のコードから拡張クラスのTime.parseを呼ぶ方法がないか調べて以下記事を参考にさせていだきました。
既存メソッドのオーバーライド
alias_method
を使って拡張クラスTimeのparse
のメソッド名を__parse__
に変更することで、先程の無限ループを回避できそうでした。
念の為、外部から呼ばれないようにprivate
で定義します。
class Time
alias_method :__parse__, :parse
private :__parse__
def self.parse(time)
if time.empty?
Time.now
else
Time.parse(time)
end
end
end
すると今度はparse
メソッドがないとエラーが出ます。
parseはクラスメソッドであるため、alias_medhodが適用できませんでした。
bundle exec rails c
`alias_method': undefined method `parse' for class `Time' (NameError)
Time.parseのモンキーパッチ & alias_methodの適用(クラスメソッド)
クラスメソッドをalias_methodする方法について以下参考にさせていただきました。
クラスメソッドに alias を付ける
class << self内でalias_method
を定義すれば適用できました。
最終形はこちらです。
class Time
class << self
alias_method :__parse__, :parse
private :__parse__
end
def self.parse(time)
if time.empty?
Time.now
else
__parse__(time)
end
end
end
以下の様にparseメソッドをclass << self内で定義しても可能です。
その際はselfは不要です。
class Time
class << self
alias_method :__parse__, :parse
private :__parse__
def parse(time)
if time.empty?
Time.now
else
__parse__(time)
end
end
end
end
最後に
いまさらrubyを1.8.7を使っている環境は少ないと思いますが、レガシーコードのバージョンアップ等で役に立てていただければと思います。
また、組み込みクラスのモンキーパッチの方法について参考にはなるかと思いますので、お役に立てたら幸いです。
参考
Ruby 1.9.2 リファレンスマニュアル Time.parse