如何在Rails中验证日期?

99

我希望能够在我的Ruby on Rails模型中验证日期,但是当它们到达我的模型时,日期、月份和年份的值就已经转换成了错误的日期。

例如,如果我在视图中输入2009年2月31日,当我在控制器中使用Model.new(params[:model])时,它会将其转换为“2009年3月3日”,这被我的模型认为是有效日期,但实际上是不正确的。

我希望能够在我的模型中进行此验证。有没有什么方法可以做到这一点,或者我完全错了?

我找到了这个 "日期验证",它讨论了这个问题,但从未得到解决。


4
这个问题目前是Rails日期验证的谷歌热门搜索结果之一。就像我最初看到时一样,你可能会错过其重要部分:所选答案中的validates_timeliness gem。我甚至没有阅读答案的其他部分,因为我在别处发现了validates_timeliness这个 gem,它可以满足我所有的需求,而无需编写任何自定义代码。 - Jason Swett
10个回答

65
我猜您正在使用date_select助手来生成日期标签。您可以通过使用选择表单助手为日、月、年字段创建标签,以另一种方式完成它。像这样(我使用的示例是创建日期字段):
<%= f.select :month, (1..12).to_a, selected: @user.created_at.month %>
<%= f.select :day, (1..31).to_a, selected: @user.created_at.day %>
<%= f.select :year, ((Time.now.year - 20)..Time.now.year).to_a, selected: @user.created_at.year %>

而在模型中,您验证日期:

attr_accessor :month, :day, :year
validate :validate_created_at

private

def convert_created_at
  begin
    self.created_at = Date.civil(self.year.to_i, self.month.to_i, self.day.to_i)
  rescue ArgumentError
    false
  end
end

def validate_created_at
  errors.add("Created at date", "is invalid.") unless convert_created_at
end
如果你正在寻找一个插件解决方案,我建议你看一下validates_timeliness插件。它的使用方法如下(来自github页面):
class Person < ActiveRecord::Base
  validates_date :date_of_birth, on_or_before: lambda { Date.current }
  # or
  validates :date_of_birth, timeliness: { on_or_before: lambda { Date.current }, type: :date }
end 

可用的验证方法列表如下:

validates_date     - validate value as date
validates_time     - validate value as time only i.e. '12:20pm'
validates_datetime - validate value as a full date and time
validates          - use the :timeliness key and set the type in the hash.

建议的解决方案对我不起作用,我正在使用Rails 2.3.5。 - Roger
我重写了它,使其更加简洁,并针对Rails 2.3.5进行了测试,这对我来说是有效的。 - Jack Chu
嗨,杰克,我尝试了你的解决方案,添加attr_accessible:day,month,year对我有效。但这对我来说没有意义...谢谢,如果你有任何想法! - benoitr
在Rails 3中,我正在使用https://github.com/adzap/validates_timeliness/,它非常棒。 - Jason Swett
validates_timeliness插件/宝石非常好用。运行良好,使我的代码更简洁。感谢您的建议。 - mjnissim

20

使用 chronic gem:

class MyModel < ActiveRecord::Base
  validate :valid_date?

  def valid_date?
    unless Chronic.parse(from_date)
      errors.add(:from_date, "is missing or invalid")
    end
  end

end

4
在这个特定的例子中,我不得不使用Chronic.parse(from_date_before_type_cast)来获取输入的字符串值。否则,Rails会对字符串进行类型转换,因为该字段是一个datetime字段。 - Calvin

18
如果您想要使用Rails 3或Ruby 1.9版本,请尝试使用date_validator gem。

我刚试了一下。只用了两分钟就安装并添加了验证! - jflores

12

Active Record提供了 _before_type_cast 属性,其中包含类型转换之前的原始属性数据。这对于使用预转换值返回错误消息或仅执行在类型转换后不可能进行的验证非常有用。

我建议避免Daniel Von Fange的建议覆盖访问器,因为在访问器中执行验证会略微改变访问器的约定。Active Record专门针对此情况提供了一项功能。请使用它。


7

虽然有点晚了,但多亏了 "How do I validate a date in rails?" ,我成功编写了这个验证器,希望它对某些人有用:

在你的 model.rb 中:

validate :date_field_must_be_a_date_or_blank

# If your field is called :date_field, use :date_field_before_type_cast
def date_field_must_be_a_date_or_blank
  date_field_before_type_cast.to_date
rescue ArgumentError
  errors.add(:birthday, :invalid)
end

1
迟到总比不来得好,而且对我来说还是及时的! - fatfrog

4

由于你需要在模型中将日期字符串转换成日期之前处理它,我建议覆盖该字段的访问器。

假设你的日期字段是published_date。请将以下内容添加到你的模型对象中:

def published_date=(value)
    # do sanity checking here
    # then hand it back to rails to convert and store
    self.write_attribute(:published_date, value) 
end

我不知道我是否会用它来达到这个目的,但这是一个很好的建议。 - Dan Rosenstark

1

Here's a non-chronic answer..

class Pimping < ActiveRecord::Base

validate :valid_date?

def valid_date?
  if scheduled_on.present?
    unless scheduled_on.is_a?(Time)
      errors.add(:scheduled_on, "Is an invalid date.")
    end
  end
end

1
这个总是返回 false: "1/1/2013".is_a?(Date) - 日期来自用户输入,因此它被作为字符串提供。我需要先将其解析为日期吗? - Mohamad
正确。Date.parse("1/1/2013").is_a?(Date),但是如果它根本无法解析,那么它也很可能不是一个日期。 - Trip
1
需要解决参数错误...啊,如果它可以自动解析字符串就太棒了...好吧,有一些宝石可以做到这一点。 - Mohamad

0
您可以这样验证日期和时间(在控制器中的某个方法中,如果您使用自定义选择,则可以访问参数)...
# Set parameters
year = params[:date][:year].to_i
month = params[:date][:month].to_i
mday = params[:date][:mday].to_i
hour = params[:date][:hour].to_i
minute = params[:date][:minute].to_i

# Validate date, time and hour
valid_date    = Date.valid_date? year, month, mday
valid_hour    = (0..23).to_a.include? hour
valid_minute  = (0..59).to_a.include? minute
valid_time    = valid_hour && valid_minute

# Check if parameters are valid and generate appropriate date
if valid_date && valid_time
  second = 0
  offset = '0'
  DateTime.civil(year, month, mday, hour, minute, second, offset)
else
  # Some fallback if you want like ...
  DateTime.current.utc
end

0
我建议使用这种方法代替
  validate :birthday_must_be_a_date

  def birthday_must_be_a_date
    birthday&.to_date || errors.add(:birthday, :blank)
  rescue Date::Error
    errors.add(:birthday, :invalid)
  end

-2

虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。如果链接页面更改,仅有链接的答案可能会失效。 - Alex
我更喜欢我的答案。两个人都同意。 - Ryan Duffield
4
@Pinal,链接确实已经失效了。 - Wayne Conrad

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接