Skip to content

Instantly share code, notes, and snippets.

@joekhoobyar
Last active August 29, 2015 13:56
Show Gist options
  • Save joekhoobyar/9235260 to your computer and use it in GitHub Desktop.
Save joekhoobyar/9235260 to your computer and use it in GitHub Desktop.
require 'date'
# @see https://gist.github.com/joekhoobyar/9235260
class DateValidator < ActiveModel::EachValidator
DEFAULT_FORMAT = '%Y-%m-%d'
# For testing against the current date.
TESTS = [ :future, :past ]
# For comparing against another date.
COMPARISONS = { before: :<, after: :>, not_before: :>=, not_after: :<= }
# All supported options.
CHECKS = TESTS + COMPARISONS.keys
RESERVED_OPTIONS = CHECKS + [:message, :allow_nil, :allow_blank, :format]
def validate(record)
attributes.each do |attr_name|
# First, we must check the raw attribute value so we can skip blank/nil values when applicable.
before_type_cast = :"#{attr_name}_before_type_cast"
if record.respond_to?(before_type_cast)
raw_value = record.send(before_type_cast)
next if (raw_value.nil? && options[:allow_nil]) || (raw_value.blank? && options[:allow_blank])
end
# Next, we check the typecasted value to see if it is actually a date.
# If the raw value is already a date, we can skip this validation
value = record.read_attribute_for_validation(attr_name) rescue nil
if value.acts_like?(:date) && ! raw_value.acts_like?(:date)
# Finally, we format the date and compare it against the raw value, to see if the entire string is consumed.
# This is necessary because Ruby's wrapper around libc's strptime does not provide the unparsed portion
# of the input string. So, we must do this to determine if the string was completely parsed.
#
# FYI: To be as safe as possible, we use case-insensitive comparisons here.
# If the strings do not match, we set the date to nil to cause an error to be reported.
#
value = nil unless raw_value.casecmp(value.strftime(options[:format]||DEFAULT_FORMAT)).zero?
end
# If we do not have a date value at this point, it is an error.
unless value.acts_like? :date
record.errors.add attr_name, options[:message]||:not_a_date, filtered_options(raw_value)
# Otherwise, we perform any additional checks.
else
options.slice(*CHECKS).each do |check, option_value|
case check
# Comparisons against another date. Options with a value that does not resolve to a date will be skipped.
when *COMPARISONS.keys
option_value = option_value.call(record) if option_value.is_a?(Proc)
option_value = record.send(option_value) if option_value.is_a?(Symbol)
unless ! option_value.acts_like?(:date) || value.send(COMPARISONS[check], option_value)
record.errors.add attr_name, check, filtered_options(value, other: option_value)
end
# Comparisons against the the current date. Option values are treated as true/false.
else
option_value, check_failed = !! option_value, ! value.send(:"#{check}?")
unless option_value ^ check_failed
record.errors.add attr_name, check_failed ? :"not_#{check}" : check, filtered_options(value)
end
end
end
end
end
end
protected
def filtered_options(value,extras={})
extras[:value] = value
opts = options.except(*RESERVED_OPTIONS)
extras.each{|k,v| opts[k] = l(v) }
opts
end
private
def l(value)
value.acts_like?(:date) ? I18n.localize(value) : value
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment