Question

I'm trying to get a text field that my users can enter in something that is parsable by the Chronic gem. Here is my model file:

require 'chronic'

class Event < ActiveRecord::Base
  belongs_to :user

  validates_presence_of :e_time
  before_validation :parse_date

  def parse_date
    self.e_time = Chronic.parse(self.e_time_before_type_cast) if self.e_time_before_type_cast
  end
end

I think that it is being called because if I misspell something in parse_date, it complains that it doesn't exist. I've also tried before_save :parse_date, but that doesn't work either.

How could I get this to work?

Thanks

Was it helpful?

Solution

This kind of situation looks like a good candidate for using virtual attributes in your Event model to represent the natural language dates and times for the view's purpose while the real attribute is backed to the database. The general technique is described in this screencast

So you might have in your model:

class Event < ActiveRecord::Base
  validates_presence_of :e_time

  def chronic_e_time
    self.e_time // Or whatever way you want to represent this
  end

  def chronic_e_time=(s)
    self.e_time = Chronic.parse(s) if s
  end
end

And in your view:

<% form_for @event do |f| %>

  <% f.text_field :chronic_e_time %>

<% end %>

If the parse fails, then e_time will remain nil and your validation will stop the record being saved.

OTHER TIPS

Building on what @bjg did, here's a working solution you can drop in config/initializers/active_record_extend.rb

module ActiveRecord
  class Base
    # Defines natural language getters/setters for date/time fields.
    #
    #   chronic_attr :published_at
    #
    # ...will get you c_published_at & c_published_at=

    def self.chronic_attr(*arguments)
      arguments.each do |arg|

        define_method "c_#{arg}=".to_sym do |dt|
          self[arg] = Chronic::parse(dt)
        end

        define_method "c_#{arg}".to_sym do 
          if self[arg]
            self[arg].to_s(:picker)
          else
            ''
          end
        end
      end
    end
  end
end

I know monkey-patching is passe these days but I think it is the most straight forward way to integrate Ruby, Rails and Chronic. I put this gist in my initializer:

# https://gist.github.com/eric1234/3739149
#
# Mass monkey-patching! Provides integration between Chronic, Ruby and
# Rails. So now these all work:
#
#     Date.parse "next summer"
#     DateTime.parse "in 3 hours"
#     Time.parse "3 months ago saturday at 5:00 pm"
#
# In addition we override String#to_date, String#to_datetime, String#to_time.
# These methods are used by older version of ActiveRecord when parsing time.
# For newer versions of ActiveRecord, Date::_parse is overridden to also
# use Chronic. This means you can assign a simple string to a ActiveRecord
# attribute:
#
#     my_obj.starts_at = "thursday last week"
#
# Also since the String method are redefined you can easily create dates
# from strings. For example if you want tomorrow at 2pm you can just do:
#
#     'tomorrow at 2pm'.to_time
#
# This is more readable than the following IMHO:
#
#     1.day.from_now.change hour: 14

module Chronic::Extensions
  module String
    def to_date
      parsed = Chronic::Extensions.safe_parse self
      return parsed.to_date if parsed
      super
    end

    def to_datetime
      parsed = Chronic::Extensions.safe_parse self
      return parsed.to_datetime if parsed
      super
    end

    def to_time
      parsed = Chronic::Extensions.safe_parse self
      return parsed.to_time if parsed
      super
    end
  end
  ::String.prepend String

  module DateTime
    def parse datetime, *args
      parsed = Chronic::Extensions.safe_parse datetime
      return parsed.to_datetime if parsed
      super
    end
  end
  ::DateTime.singleton_class.prepend DateTime

  module Date
    def _parse date, *args
      parsed = Chronic::Extensions.safe_parse(date).try :to_datetime
      if parsed
        %i(year mon mday hour min sec sec_fraction offset).inject({}) do |result, fld|
          value = case fld
            when :offset then (parsed.offset * 86400).to_i
            else parsed.public_send fld
          end
          result[fld] = value if value && value != 0
          result
        end
      else
        super
      end
    end

    def parse date, *args
      parsed = Chronic::Extensions.safe_parse date
      return parsed.to_date if parsed
      super
    end
  end
  ::Date.singleton_class.prepend Date

  module Time
    def parse time, now=self.now
      parsed = Chronic::Extensions.safe_parse time, now: now
      return parsed if parsed
      super
    end

    def zone
      super.tap do |cur|
        Chronic.time_class = cur
      end
    end

    def zone= timezone
      super.tap do
        Chronic.time_class = zone
      end
    end
  end
  ::Time.singleton_class.prepend Time

  def self.safe_parse value, options={}
    without_recursion { Chronic.parse value, options }
  end

  # There are cases where Chronic actually uses the Ruby date/time libraries.
  # This leads to infinate recursion as our monkey-patch will intercept the
  # built-in libraries to hand off to Chronic which in turn hands back to the
  # built-in libraries.
  #
  # To avoid this we have this function which acts as a guard to prevent the
  # recursion. If we have already proxied off to Chronic we won't proxy again.
  def self.without_recursion &blk
    unless in_recursion
      self.in_recursion = true
      ret = blk.call
      self.in_recursion = false
    end
    ret
  end
  mattr_accessor :in_recursion
end
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top