RailsAdminの日時選択が日本語でバグる

概略

Rails Adminでdatetime型(日時)のカラムを含むモデルを操作する際、ロケールが日本語で、かつsvenfuchs/rails-i18nのファイルをそのまま使っていると例外ArgumentError(argument out of range)で死ぬ。

原因

そもそも内部で使ってるDate._parse(これはRubyの標準ライブラリである’time’に含まれる)が日本語形式(y年m月d日)での日付のパースに対応していない。

対策

「日本語を使わなければいいんじゃないかな」

Rails AdminではjQuery UIのdatepicker/timepickerを使っています。input要素のdata-options属性にオプションをJSON形式で渡していて、このオプションの中に月名や曜日名、日付または時刻のフォーマットが含まれています。なのでこのオプションを変更して、フォーマットをDate._parseが対応している形式に変更すれば対策出来ます。このオプションは使用しているロケールに依存します(JavaScriptの実行時点ではなくてビューのテンプレートがレンダリングされた時点でオプションの中身が決定されて引き渡されます)。

が、もはや一体どこを弄ったらjQueryの(date|time)pickerに日本語が渡らないように出来るか調べる気力もなくなってしまったので、ロケール側を弄ることで対処しました。

具体的にはconfig/locales/ja.ymldate.formats、およびtime.formats以下に存在する各種日付や時刻の形式をDate._parseがパース出来、日本語を含まないような形式に変更します。%Y-%m-%dとか%Y-%m-%d %H:%M:%Sとか、数値と記号だけで表記するように変更すると幸せになれると思います。

これはロケール単位で変更出来るので、日本語のみ月名・曜日を含むような形式を避ければ問題ないと思います。英語のロケールの方では%B %d, %Y(‘January 23, 2014’)とかが標準ですが、Date._parseはこの辺の形式には対応しているので問題ないです。

または、日本語ロケールの月名・曜日名の定義をそのまま英語に差し替えてしまうという手もあります。日本語ロケールにおいてもフォーマットは英語ロケールと統一したいという場合にはこの方がいいと思います。

欠点としては、影響がRails Adminに限らずアプリ全体に及んでしまう点が挙げられます。

試してはいませんが、他にはRails Adminのビューを弄れば出来そうな予感はします。要はinput要素のdata-options属性の中身を変更出来ればいいわけなので、ビューをカスタマイズして適切な形式のフォーマットを渡すとか、あるいは単純にJavaScriptで実行時に書き換えてやるとか、いろいろやりようはありそうです。が、そもそもビューのカスタマイズが面倒そうだったのでやってません。

おまけ: 調査の経過

エラーを吐いたときのスタックトレースを見ながら各メソッドに渡っている引数を見ると、引数に渡っている文字列が2014年01Mon23Sun(Thu) 09:00になっていたのでこの辺を調べた。

日本語ロケールで使用する場合、管理画面上のdatetime pickerを使うとコントローラへは2014年01月23日(木) 09:00といった形式で選択した日時が渡る。RailsAdminはこれをRailsAdmin::Config::Fields::Types::Datetime.parse_inputメソッドを使ってパースしようとする。

ここでパースするとき、normalizeメソッドを通す。normalizeメソッドはパースしようとする文字列と、フォーマット文字列を渡す(フォーマット文字列についてはTime.strftimeの説明を見るとよさげ)。日時に関しては、概略で示した日本語ロケールのファイルの場合、これは%Y年%m月%d日(%a) %H:%Mである。normalizeメソッドからさらにメソッド呼び出しを辿ると最終的にDate._parseメソッドに文字列を渡してパースすることがわかった。

DateTimeに対応するnormalizeメソッドは、英語ロケール以外が選択されており、かつ月名(とその省略形)、曜日名(とのその省略形)、午前/午後のフォーマット文字列が含まれるときに、パース対象の文字列に含まれる英語以外のロケールのそれらを、英語ロケールに置換する。フォーマット文字列でいえば%[AaBbp]のいずれかが含まれる場合に該当する。つまり2014年01月23日(木) 09:00%Y年%m月%d日(%a) %H:%Mが渡ったときに、2014年01月23日(Thu) 09:00を返すはずである。

前述のフォーマット文字列には%a(曜日)が含まれるのでこの置換が発生するが、このとき単純に全ての曜日の文字を置換してしまう。要は「月」→"Mon"、「火」→"Tue"、「水」→"Wed"、…という置換をパース対象文字列に行うわけだが、「月」と「日」が曜日以外に現れるせいでこれが巻き込まれて形式が狂う。その結果として最初に示した2014年01Mon23Sun(Thu) 09:00になってしまう。「01月」の「月」と「23日」の「日」が置換されてしまったわけだ。

で、ここで文字列が壊れてしまったのが原因なんじゃないかと思ったが、ふとそこでデバッガを使ってパース対象文字列を強引に元の正しい文字列に直しても同じエラーが発生する。別のコンソールでpryを立ち上げて同じ文字列をDate._parseに食わせたところ、同じ結果が返ってくる。今回の場合、月の値が20になってしまっている。こうして問題は前述の置換処理でもなんでもなく、そもそも冒頭に述べたようにDate._parseが「2014年01月23日」というような日本語の形式に対応していないかららしい、ということになった。