17

I believe that datetime_select is black magic. What I'm really trying to figure out is the whole 1i,2i,3i,4i... multiple parameters stuff. Specifically how is it handled in the back end (activerecord, something else?). What's with the 'i' after the order-number? Is it a type-specifier? If so what are other types that are available? I've read the source of date_helper.rb and it's quite opaque.

Here's my motivation:

I've got a :datetime column in my model and I want to input in the view via two text_fields: one for date and one for time. They need to be validated, merged together, then stored into the datetime column. Ultimately I'll be using a javascript calendar to input dates into the date field.

So has anybody done this? I tried using virtual attributes (incredibly undocumented besides the rudimentary railscast) and the issue was that when a new activerecord object is created and has nil attributes, the virtual attribute fails (undefined method strftime for nil class, which makes sense).

Anybody have any suggestions or best practices? Thanks!

Dean Stamler
  • 382
  • 4
  • 13
  • in activerecord/base.rb [assign_multiparameter_attributes] instantiates objects for all attribute classes that needs more than one constructor parameter. This works by calling new on the column or aggregation type object with these parameters. ie: written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, f for Float, s for String, and a for Array. – Dean Stamler Sep 23 '09 at 19:22
  • 1
    Hi Dean, if you've found your own answer, the usual approach is to 'Answer My Own Question' so people can see the question has been addressed. – nfm Sep 23 '09 at 23:53
  • Can't upvote this very legitimate question enough – cseelus Oct 08 '18 at 11:47

2 Answers2

17

I had the same question. Here's what I found...

http://apidock.com/rails/ActiveRecord/Base/assign_multiparameter_attributes

assign_multiparameter_attributes(pairs) private

Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done by calling new on the column type or aggregation type (through composed_of) object with these parameters. So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the parentheses to have the parameters typecasted before they’re used in the constructor. Use i for Fixnum, f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the attribute will be set to nil.

So the numbers are for the parameters of the data type that the column is. The "i" is to convert it to an integer. Other types are supported like "f" for float, etc.

You can see here in execute_callstack_for_multiparameter_attributes that it detects the target type and instantiates it passing the values.

So, to try directly answering the posed question, I don't think you can use it to override how a DateTime is created because it uses the constructor which doesn't support the format you want to pass in. My work-around was to split the DateTime column out to one Date and one Time column. Then the UI could work the way I wanted.

I wonder if it might be possible to create attribute accessors on the class that represent the date and time separately but keep a single DateTime column. Then the form could reference those separate accessors. That might be workable. Another option might be to use composed_of to customize the type further.

Hope that helps someone else. :)

Mark Eric
  • 753
  • 1
  • 7
  • 14
2

Here's what I ended up coming up with. If anyone has any comments, let me know. I'd love to get feedback.

  validate :datetime_format_and_existence_is_valid  
  before_save :merge_and_set_datetime   

  # virtual attributes for date and time allow strings
  # representing date and time respectively to be sent
  # back to the model where they are merged and parsed
  # into a datetime object in activerecord
  def date
    if (self.datetime) then self.datetime.strftime "%Y-%m-%d"
    else @date ||= (Time.now + 2.days).strftime "%Y-%m-%d" #default
    end
  end
  def date=(date_string)
    @date = date_string.strip
  end
  def time
    if(self.datetime) then self.datetime.strftime "%l:%M %p"
    else @time ||= "7:00 PM" #default
    end
  end
  def time=(time_string)
    @time = time_string.strip
  end

  # if parsing of the merged date and time strings is
  # unsuccessful, add an error to the queue and fail
  # validation with a message
  def datetime_format_and_existence_is_valid    
    errors.add(:date, 'must be in YYYY-MM-DD format') unless
      (@date =~ /\d{4}-\d\d-\d\d/) # check the date's format
    errors.add(:time, 'must be in HH:MM format') unless # check the time's format
      (@time =~ /^((0?[1-9]|1[012])(:[0-5]\d){0,2}(\ [AaPp][Mm]))$|^(([01]\d|2[0-3])(:[0-5]\d){0,2})$/)
    # build the complete date + time string and parse
    @datetime_str = @date + " " + @time
    errors.add(:datetime, "doesn't exist") if 
      ((DateTime.parse(@datetime_str) rescue ArgumentError) == ArgumentError)
  end

  # callback method takes constituent strings for date and 
  # time, joins them and parses them into a datetime, then
  # writes this datetime to the object
  private
  def merge_and_set_datetime
    self.datetime = DateTime.parse(@datetime_str) if errors.empty?
  end
Dean Stamler
  • 382
  • 4
  • 13
  • Check out validates_timeliness for date validations, much easier. Also look at Date::DATE_FORMATS AND Time::DATE_FORMATS, for a better way of creating more formats you can re-use. I for instance add a format like Date::DATE_FORMATS[:in_words] = "%b %e, %Y" and you can then say Date.today.to_s(:in_words) in an initializer. – brad Jan 19 '10 at 17:27
  • No need to prefix self.* when you just read an attribute, so better to use "if(datetime)"... – Scholle Jul 02 '11 at 15:53
  • @Scholle I disagree, when working with other devs I like to know whether something is an attribute or a private method. This is how I signal that... – OneChillDude Mar 02 '14 at 17:31