現象
Ruby 3.2 (というか psych 4.0.5以上)と timecop 0.9.5以下を組み合わせると、YAML の Date がパースされなくなる。
gem 'psych', '4.0.5'
gem 'timecop', '0.9.5'
require 'yaml'
require 'timecop'
puts YAML.unsafe_load('2024-03-14').class
# => String
原因
psych の Psych::ScalarScanner#tokenize
で Date のパースをしているので、モンキーパッチしてエラーを見てみる。v4.0.5/lib/psych/scalar_scanner.rb#L63-L69 を一部改変したものが以下。
module Psych
# 省略...
class ScalarScanner
# 省略...
def def tokenize string
# 省略...
elsif string.match?(/^\d{4}-(?:1[012]|0\d|\d)-(?:[12]\d|3[01]|0\d|\d)$/)
require 'date'
begin
class_loader.date.strptime(string, '%F', Date::GREGORIAN)
rescue ArgumentError => e
# エラーの出力
pp e
string
end
これで動かすと、以下のようなエラーが得られる。 Timecop が Date.strptime
の第3引数で怒っていることがわかる
#<ArgumentError: Timecop's Date::strptime_with_mock_date only supports Date::ITALY for the start argument.>
Ruby 3.1 には psych 4.0.3が同梱されており、 Ruby 3.2 には psych 5.0.1が同梱されているが、psych 4.0.5 で、Date 型とみなした文字列のパース(Date.strptime
)の基準日が Date::ITARLY
から DATE::GREGORIAN
に変更される修正が行われた。
参考
timecop は Date.strptime
をモックしているが、0.9.5 以下だと Date::ITALY
以外を例外にしている。
def strptime_with_mock_date(str = '-4712-01-01', fmt = '%F', start = Date::ITALY)
unless start == Date::ITALY
raise ArgumentError, "Timecop's #{self}::#{__method__} only " +
"supports Date::ITALY for the start argument."
end
v0.9.5/lib/timecop/time_extensions.rb#L48-L51 より
解決方法
timecop を 0.9.6 以上にすればいい。以下の timecop の PRで修正されている。
Allow other "dates of calendar reform" in Date#strptime #389
gem 'psych', '4.0.5'
gem 'timecop', '0.9.6'
require 'yaml'
require 'timecop'
puts YAML.unsafe_load('2024-03-14').class
# => Date