Ruby Date.parse解析公元0年前后及前现代日期返回异常结果如何解决
原因说明
这是Ruby Date.parse 的默认行为,不是bug,核心逻辑有两点:
- 两位数/小于100的年份自动补全规则:
Date.parse会把所有0-99区间的年份值默认映射到1970-2069年的范围,这个逻辑优先级高于AD/BC/CE/BCE这类纪元标识的识别。所以传入1 BC时,解析器先拿到年份值1,直接补全为2000年,忽略了后面的BC标识;传入AD 99时拿到99,补全为1999年;传入14补全为2014年。直到年份值>=100时,才不会触发这个补全逻辑,这也是为什么AD 100能正常解析。 - 公元前年份计数规则:Ruby的Date采用天文纪年计数,这个体系里存在公元0年,对应历史上的公元前1年,所以公元前n年对应的内部存储年份值是
-(n-1),你看到27 BC返回-26是完全符合规范的,不是计算错误。
你之前尝试修改的start参数是用来设置儒略历/格里高利历的切换日期,和年份解析逻辑无关,自然不会生效。
可行解决方案
不要依赖Date.parse处理公元100年以前的日期,用以下方式规避自动补全逻辑:
- 直接构造Date对象,跳过字符串解析步骤
# 19 August 1 BC 对应天文纪年0年 Date.new(0, 8, 19) # => #<Date: 0000-08-19> # 19 August AD 99 Date.new(99, 8, 19) # => #<Date: 0099-08-19> # 16 January 27 BC 对应天文纪年-26 Date.new(-26, 1, 16) # => #<Date: -0026-01-16>
- 如果必须解析带纪元标识的字符串,自己写一层简单的转换逻辑,绕开parse的默认补全:
def parse_historical_date(str) match_data = str.match(/(\d{1,2}) ([a-zA-Z]+) (\d+) ?(BC|AD|BCE|CE)?/i) return nil unless match_data day, month_str, year_str, era = match_data.captures month = Date::MONTHNAMES.index { |m| m&.downcase == month_str.downcase } year = year_str.to_i # 处理公元前纪年转换 if era&.upcase.in?(['BC', 'BCE']) year = -(year - 1) end Date.new(year, month, day.to_i) end parse_historical_date("19 august 1 BC") # => #<Date: 0000-08-19> parse_historical_date("19 august AD 99") # => #<Date: 0099-08-19> parse_historical_date("16 January 27 BC") # => #<Date: -0026-01-16>
- 如果需要对外展示符合历史习惯的公元前年份,不要直接调用
date.year输出,单独做一层格式化即可:
def format_historical_date(date) if date.year <= 0 "#{date.day} #{Date::MONTHNAMES[date.month]} #{-date.year + 1} BC" else "#{date.day} #{Date::MONTHNAMES[date.month]} #{date.year} AD" end end format_historical_date(Date.new(-26,1,16)) # => "16 January 27 BC" format_historical_date(Date.new(0,8,19)) # => "19 August 1 BC"
内容的提问来源于stack exchange,提问作者spethspeth
相关产品推荐
相关产品推荐

